켄트 벡(Kent Back, TDD) says
이전 장 마지막에 이어서 작업한다.
바로 아래 주석 처리되어 있는 내용의 테스트 코드를 작성한다.
# 그녀는 바로 작업을 추가하기로 한다.
# "공작깃털 사기" 라고 텍스트 상자에 입력한다.
# (에디스의 취미는 날치 잡이용 그물을 만드는 것이다)
# 엔터키를 치면 페이지가 갱신되고 작업 목록에
# "1: 공작깃털 사기" 아이템이 추가된다
functional_test.py - 추가 작성한 테스트 코드
셀레늄 메소드 설명
작성한 FT를 실행해보면 의도된 실패가 발생한다.
$ python functional_test.py
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_test.py", line 21, in test_can_start_a_list_and_retrieve_it_later
header_text = self.browser.find_element_by_tag_name('h1').text
File "/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 530, in find_element_by_tag_name
return self.find_element(by=By.TAG_NAME, value=name)
File "/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 978, in find_element
'value': value})['value']
File "/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 321, in execute
self.error_handler.check_response(response)
File "/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 242, in check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"h1"}
(Session info: chrome=78.0.3904.108)
----------------------------------------------------------------------
Ran 1 test in 7.356s
FAILED (errors=1)
아래의 코드를
wibble = 3
아래 테스트로 굳이 만들 필요는 없다.
from myprogram import wibble
assert wibble == 3
단위 테스트는 로직이나 흐름제어, 설정 등을 테스트하기 위한 것이다. 현재 테스트로 구현된 HTML 테스트는 문자열 비교를 하고 있는데 이것은 상수 테스트와 유사하다.
지금부터 할 일은 HTML을 상수 문자열로 응답하고 테스트 했던 것을 Django 템플릿 기능으로 리펙터링 하는 과정이다.
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.020s
OK
Destroying test database for alias 'default'...
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E.
======================================================================
ERROR: test_home_page_returns_correct_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/superlists/lists/tests.py", line 15, in test_home_page_returns_correct_html
response = home_page(request)
File "/superlists/lists/views.py", line 6, in home_page
return render(request, 'home.html')
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/django/shortcuts.py", line 36, in render
content = loader.render_to_string(template_name, context, request, using=using)
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/django/template/loader.py", line 61, in render_to_string
template = get_template(template_name, using=using)
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/django/template/loader.py", line 19, in get_template
raise TemplateDoesNotExist(template_name, chain=chain)
django.template.exceptions.TemplateDoesNotExist: home.html
----------------------------------------------------------------------
Ran 2 tests in 0.011s
FAILED (errors=1)
Destroying test database for alias 'default'...
왜 통과되지 않는지 확인해 보자면
문제의 원인은 Django 내 settings.py 의 INSTALLED_APPS 에 lists 앱을 사용한다는 등록이 되지 않았기 때문이다. 자세한 내용은(https://docs.djangoproject.com/en/2.2/ref/settings/#installed-apps)에 언급되어 있다.
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'lists', # 추가
]
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.035s
OK
코드 리펙토링 과정을 마쳤다. 한 가지가 남았다. 테스트 코드가 아직 HTML 문자열 상수로 남아있는데 이것도 고쳐주는 것이 좋다.
(...생략...)
from django.template.loader import render_to_string
(...생략...)
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
expected_html = render_to_string('home.html')
self.assertEqual(response.content.decode(), expected_html)
response.content.decode() 의 decode() 함수는 response.content 바이트 데이터를 유니코드 문자열로 변환한다.
이것으로 바이트와 바이트를 비교 했던 것들을 문자열과 문자열로 비교하는 것으로 테스트가 바뀌었다.
Reminder - 4장 처음에 추가했던 기능 테스트가 아직은 실패하고 있는 상태
다시 기능 테스트를 돌려 보면 추가가 필요한게 있다. home.html의 <h1>
이다.
추가후에 기능 테스트를 다시 돌리면
$ python functional_test.py
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_test.py", line 25, in test_can_start_a_list_and_retrieve_it_later
inputbox = self.browser.find_element_by_id('id_new_item')
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 360, in find_element_by_id
return self.find_element(by=By.ID, value=id_)
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 978, in find_element
'value': value})['value']
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 321, in execute
self.error_handler.check_response(response)
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 242, in check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_new_item"]"}
(Session info: chrome=78.0.3904.108)
----------------------------------------------------------------------
Ran 1 test in 5.369s
FAILED (errors=1)
id 가 “idnewitem” 이라는 HTML 엘리먼트가 없다는 실패 결과를 내어준다.
이제 아래의 테스트 코드 주석을 만족시킬 구현 코드르 작성해야 한다.
# 그녀는 바로 작업을 추가하기로 한다.
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertEqual(
inputbox.get_attribute('placeholder'),
'작업 아이템 입력'
)
# "공작깃털 사기" 라고 텍스트 상자에 입력한다.
# (에디스의 취미는 날치 잡이용 그물을 만드는 것이다)
inputbox.send_keys('공작깃털 사기')
다시 기능 테스트를 실행해보자.
$ python functional_test.py
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_test.py", line 39, in test_can_start_a_list_and_retrieve_it_later
table = self.browser.find_element_by_id('id_list_table')
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 360, in find_element_by_id
return self.find_element(by=By.ID, value=id_)
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 978, in find_element
'value': value})['value']
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 321, in execute
self.error_handler.check_response(response)
File "/Users/pilhwankim/.pyenv/versions/tdd-with-python-env/lib/python3.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 242, in check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_list_table"]"}
(Session info: chrome=78.0.3904.108)
----------------------------------------------------------------------
Ran 1 test in 5.612s
FAILED (errors=1)
이번에는 id가 “idlisttable” 인 엘리먼트가 없다는 실패가 뜬다.
이제 아래의 테스트 코드 주석을 만족시킬 구현 코드르 작성해야 한다.
# 엔터키를 치면 페이지가 갱신되고 작업 목록에
# "1: 공작깃털 사기" 아이템이 추가된다
inputbox.send_keys(Keys.ENTER)
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertTrue(any(row.text == '1: 공작깃털 사기' for row in rows))
또 다시 기능 테스트를 실행해보자.
$ python functional_test.py
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_test.py", line 41, in test_can_start_a_list_and_retrieve_it_later
self.assertTrue(any(row.text == '1: 공작깃털 사기' for row in rows))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 1 test in 4.534s
FAILED (failures=1)
실패 원인이 분명치 않은데 “functional_test.py”, line 41 을 따라가 보면 기능 테스트 함수에 자세한 실패 메시지가 필요하다.
functional_test.py - 추가 작성한 메시지
python functional_test.py
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_test.py", line 44, in test_can_start_a_list_and_retrieve_it_later
'신규 작업이 테이블에 표시되지 않는다'
AssertionError: False is not true : 신규 작업이 테이블에 표시되지 않는다
----------------------------------------------------------------------
Ran 1 test in 5.785s
FAILED (failures=1)
이제 실패 메시지가 명확하게 표시된다. 남은 기능 테스트의 문제의 해결은 5장에서 다룬다.
2장부터 4장까지 죽 위와 같은 흐름으로 해왔다.
어떤 테스트라도 저 프로세스에 맞게 작성하였고 1번 테스트에 1번씩 저 프로세스를 돌게 했었다.
이 흐름도는 단위 테스트와 기능 테스트가 같이 존재할 때 어떻게 TDD를 진행하는지 나타나는 예시다.
마치 이중 for 문을 돌 때처럼
로 이해하면 편할 듯 하다.
즉 1개의 기능 테스트에 n개의 단위 테스트가 존재하며,
의 flow 를 거친다고 이해하면 편할 듯 하다.
그 동안 기능 테스트와 단위 테스트를 실제로 어떻게 적용하는지 감이 없었는데 이렇게 예제로 접하니깐 뭔가 적용할 만한거 같다.