이 포스팅에서는 클린 코드를 위한 테스트 주도 개발 e-book을 진행하며 책과 최신버전에서의 차이점이나, 설명이 생략되어 있는 부분들에 대해 기술한다.
한국어 번역 책


시작 전 필요사항

이 책에서 진행하는 TDD프로젝트는 pyenvpyenv-virtualenv를 사용하는 환경에서 진행했다.
자세한 내용은 pyenv에 대한 포스팅을 참조한다.

프로젝트는 홈 폴더의 projects폴더에 tdd폴더를 만들고, 그 내부에 가상환경을 생성한 후 진행한다. 파이썬은 3.6.1버전을 사용한다.

➜ cd ~/
➜ mkdir projects
➜ cd projects
➜ mkdir tdd
➜ cd tdd
➜ pyenv virtualenv 3.6.1 tdd
➜ pyenv local tdd

이후 pip를 이용해 django, selenium을 설치하고, brew를 이용해 Chromedriver를 설치한다. django1.11.x버전을 사용한다.

(tdd) ➜ pip install 'django<1.12'
(tdd) ➜ pip install selenium
(tdd) ➜ brew install chromedriver

Chapter01

첫 번째 functional_tests.py

그리고 첫 번째functional_tests.py는 아래와 같이 FireFoxChrome으로 변경한다. 본인은 파일이름이 너무 길다고 생각해 앞으로 ft.py파일명을 사용한다.

# ft.py
from selenium import webdriver

browser = webdriver.Chrome()
browser.get('http://localhost:8000')

assert 'Django' in browser.title

pyenv를 사용할 경우, python3 manage.py ...라고 실행할 필요없이 python manage.py ...또는 ./manage.py ...로 같은 명령을 실행 가능하다.

Starting Git Repository

책에서는 .gitignore파일을 생성하면서 git에서 파일을 추가하고 unstage로 바꾸는 여러 작업을 한다. 헷갈릴 수 있으니 .gitignore파일은 gitignore.io(macOS, Python, Pycharm, Django, Linux)의 내용을 그대로 사용하며, 이 파일의 맨 상단에 .idea/를 추가해준다 (Pycharm에서 사용하는 폴더명)
생성될 .gitignore의 내용

이후 git명령어는 아래와 같이 실행한다.

git init
git add -A
git commit -m 'First commit: First FT and basic Django config'

그 외

이후 webdriver.Firefox()를 사용하는 부분들은 전부 webdriver.Chrome()을 사용한다.


Chapter02

이 챕터에는 특별한 추가 요소가 없다


Chapter03

superlists/urls.py에 내용을 추가한 이후

from django.conf.urls import url

from lists import views

urlpatterns = [
    url(r'^$', views.home_page, name='home'),
    # url(r'^admin/', include(admin.site.urls)),
]

위 내용을 추가한 후 ./manage.py test를 실행했을 때의 결과가 다르다.

# 책
[...]
AttributeError: 'NoneType' object has no attribute 'rindex'

# 실제 결과
[...]
TypeError: view must be a callable or a list/tuple in the case of include().

Chapter04

이 챕터에는 특별한 추가 요소가 없다


Chapter05

Wiring Up Our Form to Send a POST Request

list/templates/home.html을 수정한 후

{% csrf_token %}을 넣은 후, test_home_page_returns_correct_html()에서 AssertionError가 발생한다.

FAIL: test_home_page_returns_correct_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/lhy/projects/tdd/superlists/lists/tests.py", line 17, in test_home_page_returns_correct_html
    self.assertEqual(response.content.decode(), expected_html)
AssertionError: '<htm[134 chars]t\t\t<input type=\'hidden\' name=\'csrfmiddlew[259 chars]tml>' != '<htm[134 chars]t\t\t\n\t\t\t<input type="text" name="item_tex[130 chars]tml>'

이 오류는 {% csrf_token %}에 의해 생성된 input요소의 값이 매번 달라서 발생하며, 해당 부분을 정규식으로 삭제처리해주면 정상작동한다.

# lists/tests.py

class HomePageTest(TestCase):
    pattern_input_csrf = re.compile(r'<input[^>]*csrfmiddlewaretoken[^>]*>')
    
    ...
    
    def test_home_page_returns_correct_html(self):
      request = HttpRequest()
      response = home_page(request)
      expected_html = render_to_string('home.html')
      self.assertEqual(
          re.sub(self.pattern_input_csrf, '', response.content.decode()),
          re.sub(self.pattern_input_csrf, '', expected_html)
      )

Passing Python Variables to Be Rendered in the Template

list/tests.py에서 assertEqual로 비교할 때

위와 같은 오류이며, 역시 정규식을 이용해 {% csrf_token %}에 의해 생성된 input요소를 삭제해준다.

# lists/tests.py
class HomePageTest(TestCase):
  ...
  
  def test_home_page_can_save_a_POST_request(self):
    request = HttpRequest()
    request.method = 'POST'
    request.POST['item_text'] = 'A new list item'

    response = home_page(request)

    self.assertIn('A new list item', response.content.decode())
    expected_html = render_to_string(
        'home.html',
        {
            'new_item_text': 'A new list item',
        }
    )
    self.assertEqual(
        re.sub(self.pattern_input_csrf, '', response.content.decode()),
        re.sub(self.pattern_input_csrf, '', expected_html)
    )