본문 바로가기
Development/Python

5. 사용자 입력 저장 (데이터베이스 기반 테스트)

by 신군. 2019. 7. 28.
반응형

폼으로 POST 요청 전송

POST 요청 전송을 위해 처리할 일은 다음과 같다.

  • <input> 태그에 name 속성 추가
  • <form> 태그에 method="POST" 속성 추가

lists/templates/home.html 파일을 <input> 태그에 name 속성을 추가하고 <form> 태그로 감싸도록 수정한다.

<html> <title>일정관리</title> <body> <h1>일정목록</h1> <form method="POST"> <input id="id_new_item" name="item_text" placeholder="할일을 입력하세요"/> </form> <table id="id_list_table"> </table> </body> </html>

지금까지는 매번 예상되는 테스트 실패를 만들어 이를 고쳐나갔는데 이번엔 예기치 못한 테스트 실패가 발생한다.

$ python function_tests.py E ====================================================================== ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "function_tests.py", line 36, in test_can_start_a_list_and_retrieve_it_later table = self.browser.find_element_by_id('id_list_table') File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 341, in find_element_by_id return self.find_element(by=By.ID, value=id_) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 843, in find_element 'value': value})['value'] File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 308, in execute self.error_handler.check_response(response) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response raise exception_class(message, screen, stacktrace) selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_list_table"]

분명히 HTML 문서 내용 상으로는 <table id="id_list_table"> 태그가 올바르게 존재하는데 위와 같이 에러가 발생하면 당황할 수 밖에 없다.

게다가 이러한 테스트 실패가 발생했지만 바로 브라우저가 닫히면서 정확히 그 내용을 확인하기도 어렵다. 이런 경우 디버깅을 위해 아래와 같은 방법이 있다.

  • print 문을 이용해 현재 페이지의 텍스트를 출력한다.
  • 에러가 발생한 이유를 좀 더 자세히 알 수 있도록 에러 메시지를 출력한다.
  • 사이트를 직접 수동으로 방문한다.
  • time.sleep에 충분한 시간을 두어 에러 이유를 확인할 수 있도록 한다.

여기서는 그다지 깔끔해보이진 않지만 마지막 방법 time.sleep(10)으로 충분한 시간을 두는 방법을 적용해본다.

function_tests.py 파일 수정

... 생략 ... inputbox.send_keys(Keys.ENTER) time.sleep(10) table = self.browser.find_element_by_id('id_list_table') ... 생략 ...

화면 캡처 추가할 것

CSRF 보안 오류가 발생했다. CSRF 공격이란 사용자가 POST 요청을 보내지도 않았는데 요청을 보내도록 하는 공격이다. Django는 이러한 보안 취약점을 방지하기 위해 CSRF 토큰을 지원하고 여기서는 간단히 CSRF 토큰 {% csrf_token %} 템플릿 태그를 추가하면 된다.

lists/templates/home.html 파일

<html> <title>일정관리</title> <body> <h1>일정목록</h1> <form method="POST"> <input id="id_new_item" name="item_text" placeholder="할일을 입력하세요"/> {% csrf_token %} </form> <table id="id_list_table"> </table> </body> </html> $ python function_tests.py F ====================================================================== FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "function_tests.py", line 40, in test_can_start_a_list_and_retrieve_it_later "New to-do item did not appear in table" AssertionError: False is not true : New to-do item did not appear in table ---------------------------------------------------------------------- Ran 1 test in 12.988s FAILED (failures=1)

위와 같이 기존 기능 테스트 실패 메시지를 확인하고 time.sleep(1)로 시간은 되돌린다.

function_tests.py 파일 수정

... 생략 ... inputbox.send_keys(Keys.ENTER) time.sleep(1) table = self.browser.find_element_by_id('id_list_table') ... 생략 ...

서버에서 POST 요청 처리

POST 요청으로 넘겨온 변수 값을 처리할 차례이다.

lists/tests.py 단위 테스트 파일에서 test_can_save_a_POST_request 메소드를 추가하고 test_homepage_returns_correct_html 메소드 이름을 test_uses_home_template으로 실제 테스트 내용에 알맞게 바꾼다.

from django.test import TestCase from django.urls import resolve from .views import home_page class HomePageTest(TestCase): def test_root_url_resolves_to_home_page_view(self): found = resolve('/') self.assertEqual(found.func, home_page) def test_uses_home_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html') def test_can_save_a_POST_request(self): response = self.client.post('/', data={'item_text': 'A new list item'}) self.assertIn('A new list item', response.content.decode())

item_text 변수에 A new list item 문자열 값을 담아 / 경로에 POST 요청한다. 이 때 POST 요청을 받아 실제 서버에서는 어떠한 처리도 하지 않으므로 아래와 같은 예상되는 테스트 실패를 확인할 수 있다.

$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). F.. ====================================================================== FAIL: test_can_save_a_POST_request (lists.tests.HomePageTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mairoo/Projects/goat/lists/tests.py", line 18, in test_can_save_a_POST_request self.assertIn('A new list item', response.content.decode()) AssertionError: 'A new list item' not found in '<html>\n<title>일정관리</title>\n<body>\n<h1>일정목록</h1>\n<form method="POST">\n <input id="id_new_item" name="item_text" placeholder="할일을 입력하세요"/>\n\'hidden\' name=\'csrfmiddlewaretoken\' value=\'OY8rz5sKAp3DsURyPSeO2dw0NtVU3Yca9YkVNTidGsM0Qlw9YOyoLqYfR4JNAQAq\' />\n</form>\n\n<table id="id_list_table">\n\n</table>\n</body>\n</html>\n' ---------------------------------------------------------------------- Ran 3 tests in 0.023s FAILED (failures=1) Destroying test database for alias 'default'...

이번에도 테스트만 통과하기 위한 방법으로 POST 요청으로 받은 값 그대로 돌려주는 형태로 코드를 수정한다.

from django.shortcuts import render from django.http import HttpResponse def home_page(request): if request.method == 'POST': return HttpResponse(request.POST['item_text']) return render(request, 'home.html')

request.POST['item_text'] 값을 서버에서 저장 처리하기는 커녕 그냥 그대로 넘겨줘서 아래와 같이 일단 테스트만 통과한다.

$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ... ---------------------------------------------------------------------- Ran 3 tests in 0.015s OK Destroying test database for alias 'default'... (goat) mairooui-MacBook-Pro:goat mairoo$

앞서 POST 요청으로 넘겨온 변수 값을 처리한다고 했지만 실제로는 그냥 넘어오는지만 확인한 것이고 실제로는 아무런 처리를 하지 않았다.

파이썬 뷰 변수를 템플릿으로 전달 후 렌더링

lists/templates/home.html 파일을 {{ new_item_text }} 템플릿 변수를 추가하도록 수정한다.

<html> <title>일정관리</title> <body> <h1>일정목록</h1> <form method="POST"> <input id="id_new_item" name="item_text" placeholder="할일을 입력하세요"/> {% csrf_token %} </form> <table id="id_list_table"> <tr> <td>{{ new_item_text }}</td> </tr> </table> </body> </html>

단위 테스트 lists/tests.py 파일의 test_can_save_a_POST_request 메소드를 수정한다.

def test_can_save_a_POST_request(self): response = self.client.post('/', data={'item_text': 'A new list item'}) self.assertIn('A new list item', response.content.decode()) self.assertTemplateUsed(response, 'home.html')

위와 같이 응답 객체가 home.html 템플릿 파일을 사용하는지 테스트 실시하면 당연히 앞서 우리는 HttpResponse 객체를 반환하도록 했으므로 테스트 실패한다.

$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). F.. ====================================================================== FAIL: test_can_save_a_POST_request (lists.tests.HomePageTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mairoo/Projects/goat/lists/tests.py", line 19, in test_can_save_a_POST_request self.assertTemplateUsed(response, 'home.html') File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/test/testcases.py", line 578, in assertTemplateUsed self.fail(msg_prefix + "No templates used to render the response") AssertionError: No templates used to render the response ---------------------------------------------------------------------- Ran 3 tests in 0.018s FAILED (failures=1) Destroying test database for alias 'default'...

더 이상 POST 요청 값을 그대로 반환하는 꼼수는 통하지 않으므로 뷰에서 템플릿에 해당 값을 넘겨주도록 처리한다.

lists/views.py 파일 수정

from django.shortcuts import render def home_page(request): return render(request, 'home.html', { 'new_item_text': request.POST['item_text'] })

이제 단위 테스트를 실시하면 아래와 같이 test_uses_home_template 테스트에서 **예기치 못한 에러가 발생한다.

$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ..E ====================================================================== ERROR: test_uses_home_template (lists.tests.HomePageTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/utils/datastructures.py", line 83, in __getitem__ list_ = super(MultiValueDict, self).__getitem__(key) KeyError: 'item_text' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/mairoo/Projects/goat/lists/tests.py", line 13, in test_uses_home_template response = self.client.get('/') File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/test/client.py", line 536, in get **extra) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/test/client.py", line 340, in get return self.generic('GET', path, secure=secure, **r) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/test/client.py", line 416, in generic return self.request(**r) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/test/client.py", line 501, in request six.reraise(*exc_info) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/utils/six.py", line 686, in reraise raise value File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/core/handlers/exception.py", line 41, in inner response = get_response(request) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/core/handlers/base.py", line 187, in _get_response response = self.process_exception_by_middleware(e, request) File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/core/handlers/base.py", line 185, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/Users/mairoo/Projects/goat/lists/views.py", line 6, in home_page 'new_item_text': request.POST['item_text'] File "/Users/mairoo/.pyenv/versions/3.6.2/envs/goat/lib/python3.6/site-packages/django/utils/datastructures.py", line 85, in __getitem__ raise MultiValueDictKeyError(repr(key)) django.utils.datastructures.MultiValueDictKeyError: "'item_text'" ---------------------------------------------------------------------- Ran 3 tests in 0.028s FAILED (errors=1) Destroying test database for alias 'default'...

test_uses_home_template 테스트는 요청이기 때문에 item_text 변수가 존재하지 않아 이렇게 테스트를 실패한다. 따라서 키 값이 존재하지 않을 경우 예외처리를 위해 아래와 같이 뷰를 수정한다.

lists/views.py 파일 수정

from django.shortcuts import render def home_page(request): return render(request, 'home.html', { 'new_item_text': request.POST.get('item_text', '') })

이제 단위 테스트는 성공하고 기능 테스트는 아래와 같은 내용으로 실패한다.

$ python function_tests.py F ====================================================================== FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "function_tests.py", line 40, in test_can_start_a_list_and_retrieve_it_later "New to-do item did not appear in table" AssertionError: False is not true : New to-do item did not appear in table ---------------------------------------------------------------------- Ran 1 test in 4.119s FAILED (failures=1)

위와 같이 단순히 에러 메시지만 출력하면 나중에 디버깅하기 어려우므로 아래와 같이 테이블의 내용을 함께 출력하도록 수정할 수 있다.

function_tests.py 파일 수정

... 생략 ... self.assertTrue( any(row.text == '1: 시장에서 미역 사기' for row in rows), "New to-do item did not appear in table. Contents were:\n{}".format(table.text), ) ... 생략 ...

실제로 교재에서는 파이썬 3.6부터 지원하는 f-string 문법을 소개하고 있지만 그래도 호환성을 위해 구 문법으로 작성했다.

그리고 기능 테스트 실시 결과에서 아래와 같이 테이블의 내용을 같이 확인할 수 있다.

python function_tests.py F ====================================================================== FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "function_tests.py", line 40, in test_can_start_a_list_and_retrieve_it_later "New to-do item did not appear in table. Contents were:\n{}".format(table.text), AssertionError: False is not true : New to-do item did not appear in table. Contents were: 시장에서 미역 사기 ---------------------------------------------------------------------- Ran 1 test in 3.873s FAILED (failures=1)

삼진 아웃과 리팩토링

복사 붙여넣기는 매우 빠른 문제 해결 방법이다. 그러나 같은 코드 부분이 세 군데 이상 나타난다면 함수로 만들어 중복을 제거해야 한다. 이를 삼진 아웃이라고 한다.

내용에 들어가기 전에 앞서 지금까지 내용을 커밋한다.

Django ORM과 첫 번째 모델 작성

POST 요청 처리 후 리다이렉트

템플릿에서 일정 목록 렌더링

데이터베이스 마이그레이션 생성

요약 및 의견

반응형