[TDD] Test-Driven Development with Python 6장

SUNGJIN KIM·2022년 7월 13일
0

tdd-with-python

목록 보기
6/7
post-thumbnail

˗ˋˏ♡ˎˊ˗ 최소 동작 사이트 구축

  • 기능 테스트 내에서 테스트 격리
  • 필요한 경우에는 최소한의 설계를
  • TDD를 이용한 새로운 설계 반영하기
  • 새로운 설계를 위한 반복
  • Django 테스트 클라이언트를 이용한 뷰, 템플릿, URL 동시 테스트
  • 목록 아이템을 추가하기 위한 URL과 뷰
  • 모델 조정하기
  • 각 목록이 하나의 고유 URL을 가져야 한다
  • 기존 목록에 아이템을 추가하기 위한 또 다른 뷰
  • URL includes를 이용한 마지막 리팩터링

기능 테스트 내에서 테스트 격리

기능 테스트를 진행할 때마다 앞에 테스트 진행했던 목록 아이템이 데이터베이스에 남아있는 문제가 있다.

이를 해결하기 위해 기존 결과물에 정리하는 코드를 넣어주면 된다.
책에서는 Django 1.4와 1.6버전에 대한 설명이 나와있는데 지금 현재 사용하는 Django 버전은 3.9.13 버전이므로 그에 맞춰 코드를 변경해주도록 한다.

책에 나와있는대로 일단 기능 테스트용 별도 폴더를 만들고, 해당 폴더 안에
tests.py와 __init__.py을 생성할 수 있도록 한다.

이렇게 변경했으면, 이후부터는 python3 manage.py test functional_tests를 실행한다.

LiveServerTestCase를 사용하기 위해, functinoal_tests/tests.py 파일을 편집하여 NewVisitorTest 클래스를 변경해준다.

functional_tests/tests.py

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import unittest
from selenium.webdriver.common.by import By
import time
from django.test import LiveServerTestCase


class NewVisitorTest(LiveServerTestCase):

    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.implicitly_wait(3)

    def tearDown(self):
        self.browser.quit()

    def check_for_row_in_list_table(self, row_text):
        table = self.browser.find_element(By.ID, 'id_list_table')
        rows = table.find_elements(By.TAG_NAME,'tr')
        self.assertIn(row_text,[row.text for row in rows])

    def test_can_start_a_list_and_retrieve_it_later(self):
        # 에디스(Edith)는 멋진 작업 목록 온라인 앱이 나왔다는 소식을 듣고
        # 해당 웹 사이트를 확인하러 간다.

        self.browser.get(self.live_server_url)

        # 웹 페이지 타이틀과 헤더가 'To-Do'를 표시하고 있다.
        self.assertIn('To-Do', self.browser.title)
        header_text = self.browser.find_elements(By.TAG_NAME, 'h1')
        for i in header_text:
            self.assertIn('To-Do', i.text)

        # 그녀는 바로 작업을 추가하기로 한다
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            '작업 아이템 입력'
        )

        # "공작깃털 사기" 라고 텍스트 상자에 입력한다
        # (에디스의 취미는 날치 잡이용 그물을 만드는 것이다)
        inputbox.send_keys('공작깃털 사기')
        

        # 엔터키를 치면 페이지가 갱신되고 작업 목록에
        # "1: 공작깃털 사기" 아이템이 추가된다
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)
        self.check_for_row_in_list_table('1: 공작깃털 사기')


        # 추가 아이템을 입력할 수 있는 여분의 텍스트 상자가 존재한다.
        # 다시 "공작깃털을 이용해서 그물 만들기" 라고 입력한다 
        # (에디스는 매우 체계적인 사람이다)
        inputbox = self.browser.find_element(By.ID,'id_new_item')
        inputbox.send_keys('공작깃털을 이용해서 그물 만들기')
        inputbox.send_keys(Keys.ENTER)

        # 페이지는 다시 갱신되고, 두 개 아이템이 목록에 보인다
        time.sleep(1)
        self.check_for_row_in_list_table('1: 공작깃털 사기')
        self.check_for_row_in_list_table('2: 공작깃털을 이용해서 그물 만들기')

        # 에디스는 사이트가 입력한 목록을 저장하고 있는지 구감하다
        # 사이트는 그녀를 위한 특정 URL을 생성해준다
        # 이때 URL에 대한 설명도 함께 제공된다

        # 해당 URL에 접속하면 그녀가 만든 작업 목록이 그대로 있는 것을 확인할 수 있다
        self.fail('Finish the test!')
        # 만족하고 잠자리에 든다.


# if __name__ == '__main__':
#     unittest.main(warnings='ignore')

맨 끝에 있는 해당 구문은 지워도 된다. FT 를 실행하기 위해 Django 테스트 실행자를 사용하기때문에 사용하지 않는다.

 if __name__ == '__main__':
     unittest.main(warnings='ignore')하세요

이렇게 까지 진행했다면, 이제 다중 작업 목록에 대해 고민해보도록 한다.

우리는 시스템 확장을 통해 각 사용자가 만든 목록을 서로 보지 못하도록 해야한다.
또한 각 사용자는 개별 URL을 가지고 있어서 이 URL을 통해 신이 저장한 목록에 접근할 수 있어야 한다.

필요한 경우에는 최소한의 설계를

최소한의 기능을 가진 작업 목록 앱을 어떻게 설계할 지 생각해본다.

1) 각 사용자가 개별 목록을 저장하도록 한다. (지금은 최소 하나의 목록을 유지할 수 있도록 한다.)
2) 하나의 목록은 여러 개의 작업 아이템으로 구성된다. 이 아이템들은 작업 내용을 설명하는 텍스트다.
3) 다음 방문 시에도 목록을 확인할 수 있도록 목록을 저장해두어야 한다. 현 시점에서는 각 목록에 해당하는 개별 URL을 사용자에게 제공하도록 한다. 이후에는 사용자를 자동으로 인식해서 해당 목록을 보여주도록 수정할 필요가 있다.

현재 MVC에서 모델 부분은 설계를 했다.
뷰와 컨트롤러는 어떻게 해야 할까?

Rest(Representational State Transfer)는 웹 설계 방법 중 하나다. 웹 기반 API를 이용해서 설계하도록 유도한다.

REST는 데이터 구조를 URL 구조에 일치시키는 방식이다. 이 앱에선 작업 목록과 아이템을 URL 구조로 표현할 수 있다.

TDD 를 이용한 새로운 설계 반영하기

TDD 처리 흐름도를 바탕으로 기존 코드를 다시 작성해서 신규 설계를 반영해준다. (사용자에게는 동일한 기능 제공)
설계 내용을 기능 테스트에 반영해보도록 한다.

functiaonl_tests/tests.py

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import unittest
from selenium.webdriver.common.by import By
import time
from django.test import LiveServerTestCase


class NewVisitorTest(LiveServerTestCase):

    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.implicitly_wait(3)

    def tearDown(self):
        self.browser.quit()

    def check_for_row_in_list_table(self, row_text):
        table = self.browser.find_element(By.ID, 'id_list_table')
        rows = table.find_elements(By.TAG_NAME,'tr')
        self.assertIn(row_text,[row.text for row in rows])

    def test_can_start_a_list_and_retrieve_it_later(self):
        # 에디스(Edith)는 멋진 작업 목록 온라인 앱이 나왔다는 소식을 듣고
        # 해당 웹 사이트를 확인하러 간다.
        # self.browser.get('http://localhost:8000')
        self.browser.get(self.live_server_url)

        # 웹 페이지 타이틀과 헤더가 'To-Do'를 표시하고 있다.
        self.assertIn('To-Do', self.browser.title)
        header_text = self.browser.find_elements(By.TAG_NAME, 'h1')
        for i in header_text:
            self.assertIn('To-Do', i.text)

        # 그녀는 바로 작업을 추가하기로 한다
        # inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox = self.browser.find_element(By.ID, 'id_new_item')
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            '작업 아이템 입력'
        )

        # "공작깃털 사기" 라고 텍스트 상자에 입력한다
        # (에디스의 취미는 날치 잡이용 그물을 만드는 것이다)
        inputbox.send_keys('공작깃털 사기')
        

        # 엔터키를 치면 페이지가 갱신되고 작업 목록에
        # "1: 공작깃털 사기" 아이템이 추가된다
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)
        edith_list_url = self.browser.current_url
        self.assertRegex(edith_list_url,'/lists/.+')
        self.check_for_row_in_list_table('1: 공작깃털 사기')

        # 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),
        # #     "신규 작업이 테이블에 표시되지 않는다 -- 해당 텍스트 :\n%s" % (
        # #         table.text,
        # #     )
        # # )
        # self.assertIn('1: 공작깃털 사기', [row.text for row in rows])

        # 추가 아이템을 입력할 수 있는 여분의 텍스트 상자가 존재한다.
        # 다시 "공작깃털을 이용해서 그물 만들기" 라고 입력한다 
        # (에디스는 매우 체계적인 사람이다)
        inputbox = self.browser.find_element(By.ID,'id_new_item')
        inputbox.send_keys('공작깃털을 이용해서 그물 만들기')
        inputbox.send_keys(Keys.ENTER)

        # 페이지는 다시 갱신되고, 두 개 아이템이 목록에 보인다
        time.sleep(1)
        self.check_for_row_in_list_table('1: 공작깃털 사기')
        self.check_for_row_in_list_table('2: 공작깃털을 이용해서 그물 만들기')
        
        # 새로운 사용자인 프란시스가 사이트에 접속한다.
        ## 새로운 브라우저 세션을 이용해서 에디스의 정보가
        ## 쿠키를 통해 유입되는 것을 방지한다.
        self.browser.quit()
        self.browser = webdriver.Firefox()

        # 프란시스가 홈페이지에 접속한다
        # 에디스의 리스트는 보이지 않는다
        self.browser.get(self.live_server_url)
        page_text = self.browser.find_element(By.TAG_NAME,'body').text
        self.assertNotIn('공작깃털 사기',page_text)
        self.assertNotIn('그물 만들기',page_text)

        # 프란시스가 새로운 작업 아이템을 입력하기 시작한다
        # 그는 에디스보다 재미가 없다
        inputbox = self.browser.find_element(By.ID,'id_new_item')
        inputbox.send_keys('우유 사기')
        inputbox.send_keys(Keys.ENTER)

        # 프란시스가 전용 URL을 취득한다.
        francis_list_url = self.browser.current_url
        self.assertRegex(francis_list_url,'/lists/.+')
        self.assertNotEqual(francis_list_url,edith_list_url)

        # 에디스가 입력한 흔적이 없다는 것을 다시 확인한다.
        page_text = self.browser.find_element(By.TAG_NAME,'body').text
        self.assertNotIn('공작깃털 사기',page_text)
        self.assertIn('우유 사기',page_text)

        # 둘 다 만족하고 잠자리에 든다.

이렇게 작성했을때, 오류가 발생하게 된다.
(도서에 쓰여진 것 말고도 아마 find_element 관련 오류도 발생할 것으로 예상되는데 이건 지금 먼저 발생하는 오류를 수정하고 그 다음에 처리할 예정이다.)


.......F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (functional_tests.tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests/tests.py", line 53, in test_can_start_a_list_and_retrieve_it_later
    self.assertRegex(edith_list_url,'/lists/.+')
AssertionError: Regex didn't match: '/lists/.+' not found in 'http://localhost:57751/'

----------------------------------------------------------------------
Ran 8 tests in 4.544s

FAILED (failures=1)
Destroying test database for alias 'default'...

일단 현재상태에서 커밋한 후에 그다음에 새로운 모델과 뷰를 만들어보도록 한다.

새로운 설계를 위한 반복

각 단계별로 새로운 설계를 위해 조금씩 변경해보도록 한다.
현재 우리가 해야할 작업은 총 4가지다.

Regexp 불일치 문제를 가지고 있는 FT가 두 번째 아이템에 개별 URL과 식별자를 적용하는 것

lists/test.py

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'],'/lists/the-only-list-in-the-world/')       

현재는 하나의 목록만 지원하기 때문에, 하나의 URL에만 이용하는 것이 맞다.
조금 더 진도를 진행해가며 다중 목록과 메인페이지를 위한 URL을 추가해가면 된다.
URL 설계는 RESTful 설계 방식을 따른다.

단위 테스트 실행 시 아래와 같이 실패하게 된다.

Found 7 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...F...
======================================================================
FAIL: test_home_page_redirects_after_POST (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "lists/tests.py", line 74, in test_home_page_redirects_after_POST
    self.assertEqual(response['location'],'/lists/the-only-list-in-the-world/')
AssertionError: '/' != '/lists/the-only-list-in-the-world/'
- /
+ /lists/the-only-list-in-the-world/


----------------------------------------------------------------------
Ran 7 tests in 0.008s

FAILED (failures=1)
Destroying test database for alias 'default'...

lists/views.py

def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/lists/the-only-list-in-the-world/')

    items = Item.objects.all()
    return render(request,'home.html',{'items' : items})

이렇게 수정하고 단위테스트와 기능 테스트를 진행했을때
단위 테스트는 성공하지만 기능 테스트는 실패함을 알 수 있다.

현재 해당 URL이 존재하지 않아서 발생하는 문제로, 특수한 URL을 구축해본다.

Django 테스트 클라이언트를 이용한 뷰,템플릿,URL 동시 테스트

Django 테스트 클라이언트를 사용하기 위해, lists/tests.py 를 열어서 ListViewTest라는 새로운 테스트 클래스를 추가한다.

lists/tests.py

class ListViewTest(TestCase):
    def test_display_all_times(self):
        Item.objects.create(text='itemey 1')
        Item.objects.create(text='itemey 2')

        response = self.client.get('/lists/the-only-list-in-the-world/')

        self.assertContains(response,'itemey 1')
        self.assertContains(response,'itemey 2')

이렇게 수정하고 테스트를 진행해봤을때 다음과 같이 결과가 나온다.

AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404 (expected 200)

아직 신규 사이트가 존재하지 않으므로 superlists/urls.py 를 수정해서 해당 문제를 해결한다.

from django.contrib import admin
from django.urls import path, include
from lists import views

# urlpatterns = [
#     path('admin/', admin.site.urls),
# ]

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/the-only-list-in-the-world/$', views.view_list, name='view_list'),

]

이렇게 코드를 수정하고 테스트를 다시 실행하면 아래와 같은 결과를 얻게된다.

    path('lists/the-only-list-in-the-world/$', views.view_list, name='view_list'),
AttributeError: module 'lists.views' has no attribute 'view_list'

이를 해결하기 위해 lists/view.py 내 임시 뷰 함수를 생성한다.

def view_list(request):
    pass

이후 단위 테스트 실행 시 아래와 같은 메시지를 확인할 수 있다.

ValueError: The view lists.views.view_list didn't return an HttpResponse object. It returned None instead.

home_page 뷰에 있는 마지막 두줄을 복사하여 효과가 있는지 확인해본다.

def view_list(request):
    items = Item.objects.all()
    return render(request,'home.html',{'items' : items})

이렇게 변경하고 테스트 진행 시 단위테스트가 성공하는 것을 확인할 수 있다.

기능 테스트의 경우 아래와 같이 결과가 나온다.

    self.assertIn(row_text,[row.text for row in rows])
AssertionError: '2: 공작깃털을 이용해서 그물 만들기' not found in ['1: 공작깃털 사기']

현재 두 개의 뷰가 존재한다.

하나는 홈페이지용 뷰고, 하나는 개별 목록용 뷰다.

이 두개는 공통 템플릿을 이용하고 있으며 데이터베이스에 있는 모든 작업 아이템을 이 템플릿에 전달하고 있다.

단위 테스트에 있는 메소드들을 살펴보면 수정이 필요한 부분을 발견할 수 있다.

$ grep -E "class|def" lists/tests.py
# class SmokeTest(TestCase):
#     def test_bad_maths(self):
class HomePageTest(TestCase):
    def remove_csrf(html_code):
    def assertEqualExceptCSRF(self, html_code1, html_code2):
    def assertInExceptCSRF(self, html_code1,html_code2):
    def test_root_url_resolves_to_home_page_view(self):
    def test_home_page_returns_correct_html(self):
    def test_home_page_can_save_a_POST_request(self):
    def test_home_page_redirects_after_POST(self):
    def test_home_page_only_saves_item_when_necessary(self):
    def test_home_page_displays_all_list_times(self):
class ItemModelTest(TestCase):
    def test_saving_and_retrieving_items(self):
class ListViewTest(TestCase):
    def test_display_all_times(self):

test_home_page_displays_all_lists_items가 필요없는 것은 명확하기 때문에 해당 코드를 삭제하고 실행하면 Ran 7을 볼 수 있다.

모든 아이템을 표시하기 위한 메인 페이지가 더 이상 필요하지 않기 때문이다.
작업 아이템을 입력할 수 있는 텍스트 상자 하나면 된다.

메인 페이지와 목록 뷰 기능이 다르기 때문에 각각 별개의 HTML 템플릿을 사용하는 것이 좋다.

lists/tests.py

class ListViewTest(TestCase):

    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')

lists/views.py

def view_list(request):
    items = Item.objects.all()
    return render(request,'list.html',{'items' : items})

해당 코드를 수정하게되면 아래와 같이 결과가 나온다.

    raise TemplateDoesNotExist(template_name, chain=chain)
django.template.exceptions.TemplateDoesNotExist: list.html

lists/templates/list.html 을 추가한다.

추가하고 나서 다시 단위 테스트 진행 시 아래와 같은 에러 메시지가 출력된다.

AssertionError: False is not true : Couldn't find 'itemey 1' in response

개별 목록을 위한 템플릿은 home.html에 있는 많은 부분을 재사용하므로 home.html을 복사해서 수정해가도록 한다.

$ cp lists/templates/home.html lists/templates/lists.html

정상적으로 복사된 것을 확인할 수 있다.

테스트가 정상적으로 통과된다면, 필요없는 코드를 삭제하거나 변경해주도록 한다.

lists/templates/home.html

<body>
    <h1>작업 목록 시작</h1>
    <form method="POST">
        <input name="item_text" id="id_new_item" placeholder="작업 아이템 입력" />
        {% csrf_token %}
    </form>

home_page 뷰가 모든 작업 아이템을 home.html 템플릿에 전달할 필요가 없기 때문에 다음과 같이 단순화 시켜준다.

lists/views.py

def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/lists/the-only-list-in-the-world/')
    # items = Item.objects.all()
    return render(request,'home.html')

코드를 수정하고, 단위 테스트와 기능테스트를 진행하면 다음과 같다.
단위 테스트의 경우에는 성공하나, 기능 테스트는 아래와 같은 메시지를 출력해준다.

    self.assertIn(row_text,[row.text for row in rows])
AssertionError: '2: 공작깃털을 이용해서 그물 만들기' not found in ['1: 공작깃털 사기']

아직 두 번째 아이템 입력이 실패되고 있다. 이유는 신규 아이템 폼이 action= 속성을 가지고 있지 않기 때문이다.

이에 lists/templates/list.html의 폼 부분을 수정해준다.

    <form method="POST" action="/">
        <input name="item_text" id="id_new_item" placeholder="작업 아이템 입력" />
        {% csrf_token %}
    </form>

이후 단위 테스트를 실행하게되면, 이전 상태로 돌아오게 된다.

    self.assertNotEqual(francis_list_url,edith_list_url)
AssertionError: 'http://localhost:59592/lists/the-only-list-in-the-world/' == 'http://localhost:59592/lists/the-only-list-in-the-world/'

일단 이로써 리팩터링을 무사히 마칠 수 있게 되었다.

목록 아이템을 추가하기 위한 URL과 뷰

신규 목록 생성을 위해 test_home_page_can_save_a_POST_request와 test_home_page_redirects_after_POST 메소드를 새로운 클래스로 이동한다.

lists/test.py

from urllib import response
from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
from django.http import HttpRequest
from django.template.loader import render_to_string
import re
from lists.models import Item

class HomePageTest(TestCase):

    @staticmethod
    def remove_csrf(html_code):
        csrf_regex = r'<input[^>]+csrfmiddlewaretoken[^>]+>'
        return re.sub(csrf_regex, '', html_code)

    def assertEqualExceptCSRF(self, html_code1, html_code2):
        return self.assertEqual(
            self.remove_csrf(html_code1),
            self.remove_csrf(html_code2)
        )

    def assertInExceptCSRF(self, html_code1,html_code2):
        return self.assertIn(self.remove_csrf(html_code1),self.remove_csrf(html_code2))

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')
        self.assertEqual(found.func, home_page)

    def test_home_page_returns_correct_html(self):
        request = HttpRequest()
        response = home_page(request)
        expected_html = render_to_string('home.html')
        self.assertTrue(response.content.decode(), expected_html)

    def test_home_page_only_saves_item_when_necessary(self):
        request = HttpRequest()
        home_page(request)
        self.assertEqual(Item.objects.count(),0)

class ItemModelTest(TestCase):
    def test_saving_and_retrieving_items(self):
        first_item = Item()
        first_item.text = '첫 번째 아이템'
        first_item.save()
        second_item = Item()
        second_item.text = '두 번째 아이템'
        second_item.save()

        saved_items = Item.objects.all()
        self.assertEqual(saved_items.count(),2)

        first_saved_item = saved_items[0]
        second_saved_item = saved_items[1]
        self.assertEqual(first_saved_item.text, '첫 번째 아이템')
        self.assertEqual(second_saved_item.text,'두 번째 아이템')

class ListViewTest(TestCase):

    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')

    def test_display_all_times(self):
        Item.objects.create(text='itemey 1')
        Item.objects.create(text='itemey 2')

        response = self.client.get('/lists/the-only-list-in-the-world/')

        self.assertContains(response,'itemey 1')
        self.assertContains(response,'itemey 2')

class NewListTest(TestCase):

    def test_saving_a_POST_request(self):
        request = HttpRequest()
        request.method = 'POST'
        request.POST['item_text'] = '신규 작업 아이템'

        response = home_page(request)

        self.assertEqual(Item.objects.count(),1)
        new_item = Item.objects.first()
        self.assertEqual(new_item.text,'신규 작업 아이템')

    def test_redirects_after_POST(self):
        request = HttpRequest()
        request.method = 'POST'
        request.POST['item_text'] = '신규 작업 아이템'

        response = home_page(request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'],'/lists/the-only-list-in-the-world/')

Django 클라이언트 테스트를 이용하여 다음과 같이 수정한다.
lists/test.py

class NewListTest(TestCase):

    def test_saving_a_POST_request(self):
        self.client.post(
            '/lists/new',
            data={'item_text':'신규 작업 아이템'}
        )
        self.assertEqual(Item.objects.count(),1)
        new_item=Item.objects.first()
        self.assertEqual(new_item.text,'신규 작업 아이템')

    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text':'신규 작업 아이템'}
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'],'/lists/the-only-list-in-the-world/')

단위 테스트를 진행하게되면 다음과 같이 결과가 출력된다.
현재 데이터베이스에 아이템을 저장할수 없는 메시지와 뷰가 302상태 코드 대신 404를 반환하고 있다는 메시지이다.
아직 lists/new가 구축되지 않아서 404응답을 받고있다.

    self.assertEqual(response.status_code, 302)
AssertionError: 404 != 302

    self.assertEqual(Item.objects.count(),1)
AssertionError: 0 != 1

신규 URL을 설정해보자.

lists/urls.py

urlpatterns = [
    # Examples : 
    # path('', views.home, name='home'),
    path('lists/the-only-list-in-the-world/', views.view_list, name='view_list'),
    path('', views.home_page, name='home'),
    path('lists/new', views.new_list, name = 'new_lists'),
]

이후, lists/views.py를 다음과 같이 수정한다.

lists/views.py

def new_list(request):
    pass

이렇게 진행하게되면 익숙히 봤던 오류가 발생할 것이다. (위에 한번 본 적 있음)

코드를 수정한다.

def new_list(request):
    return redirect('/lists/the-only-list-in-the-world')

이렇게 수정하고 단위 테스트를 진행하면 아래와 같이 에러가 발생한다.

AssertionError: '/lists/the-only-list-in-the-world' != '/lists/the-only-list-in-the-world/'
- /lists/the-only-list-in-the-world
+ /lists/the-only-list-in-the-world/
?                                  +
    self.assertEqual(Item.objects.count(),1)
AssertionError: 0 != 1

일단 0 != 1에러부터 먼저 해결해보도록 한다.

lists/views.py

def new_list(request):
    Item.objects.create(text=request.POST['item_text'])
    return redirect('/lists/the-only-list-in-the-world')

코드를 수정하고 테스트를 진행하면 위 오류는 해결이 되었으나 아래의 오류메시지가 하나더 출력된다.

    self.assertEqual(response['location'],'/lists/the-only-list-in-the-world/')
AssertionError: '/lists/the-only-list-in-the-world' != '/lists/the-only-list-in-the-world/'
- /lists/the-only-list-in-the-world
+ /lists/the-only-list-in-the-world/
?                                  +

이는 Django 테스트 클라이언트가 뷰 함수에서 약간 다른 방식으로 동작하기 때문이다.
Django가 제공하는 헬퍼 함수를 사용하도록 한다.

lists/tests.py

    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text':'신규 작업 아이템'}
        )
        self.assertRedirects(response, '/lists/the-only-list-in-the-world/')

이렇게 변경하면 테스트를 통과하게 된다.
이제, 필요없는 코드를 삭제하여 테스트를 많이 단순화 시켜보도록 한다.

if request.method == 'POST' 부분을 모두 제거할 수 있는지 확인해본다.

def home_page(request):
    return render(request,'home.html')

제거 후 테스트 진행 시, 성공하는 것을 확인했다.
이후 test_home_page_only_saves_items_when_necessary 테스트도 삭제해주도록 한다.

재 실행 시, 정상적으로 성공하는 것을 확인했다.

마지막으로 새로운 URL을 두 개 폼과 연동시키도록 한다.
home.html, lists.html 둘다 수정해야 한다.

    <form method="POST" action="/lists/new">

이렇게 하고 기능 테스트를 실행했을때, 이전과 같은 결과가 나오는 것을 확인했다.

    self.assertNotEqual(francis_list_url,edith_list_url)
AssertionError: 'http://localhost:60720/lists/the-only-list-in-the-world/' == 'http://localhost:60720/lists/the-only-list-in-the-world/'

모델 조정하기

URL 관련 정비를 마쳤기때문에, 이제는 모델을 변경해야 한다.
모델 단위 테스트를 조정한다.

이번에는 어떤 코드를 수정해야하는지 책에 나와있지 않기 때문에, 내가 스스로 찾아야 하는 시간이다.

일단 수정작업이 필요한 곳을 비교해보며 진행해보도록 한다.

diff 형식을 잘 모르기때문에 관련하여 검색해봤다.
이를 참고하면 될 듯 하다.

lists/test.py

from urllib import response
from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
from django.http import HttpRequest
from django.template.loader import render_to_string
import re
from lists.models import Item,List

class HomePageTest(TestCase):

    @staticmethod
    def remove_csrf(html_code):
        csrf_regex = r'<input[^>]+csrfmiddlewaretoken[^>]+>'
        return re.sub(csrf_regex, '', html_code)

    def assertEqualExceptCSRF(self, html_code1, html_code2):
        return self.assertEqual(
            self.remove_csrf(html_code1),
            self.remove_csrf(html_code2)
        )

    def assertInExceptCSRF(self, html_code1,html_code2):
        return self.assertIn(self.remove_csrf(html_code1),self.remove_csrf(html_code2))

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')
        self.assertEqual(found.func, home_page)

    def test_home_page_returns_correct_html(self):
        request = HttpRequest()
        response = home_page(request)
        expected_html = render_to_string('home.html')
        self.assertTrue(response.content.decode(), expected_html)

class ListAndItemModelsTest(TestCase):
    def test_saving_and_retrieving_items(self):
        list_ = List()
        list_.save()

        first_item = Item()
        first_item.text = '첫 번째 아이템'
        first_item = list_
        first_item.save()

        second_item = Item()
        second_item.text = '두 번째 아이템'
        second_item = list_
        second_item.save()

        saved_items = Item.objects.all()
        self.assertEqual(saved_items.count(),2)

        first_saved_item = saved_items[0]
        second_saved_item = saved_items[1]
        self.assertEqual(first_saved_item.text, '첫 번째 아이템')
        self.assertEqual(first_saved_item,list_)
        self.assertEqual(second_saved_item.text,'두 번째 아이템')
        self.assertEqaul(second_saved_item,list_)

class ListViewTest(TestCase):

    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')

    def test_display_all_times(self):
        Item.objects.create(text='itemey 1')
        Item.objects.create(text='itemey 2')

        response = self.client.get('/lists/the-only-list-in-the-world/')

        self.assertContains(response,'itemey 1')
        self.assertContains(response,'itemey 2')

class NewListTest(TestCase):

    def test_saving_a_POST_request(self):
        self.client.post(
            '/lists/new',
            data={'item_text':'신규 작업 아이템'}
        )
        self.assertEqual(Item.objects.count(),1)
        new_item=Item.objects.first()
        self.assertEqual(new_item.text,'신규 작업 아이템')

    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text':'신규 작업 아이템'}
        )
        self.assertRedirects(response, '/lists/the-only-list-in-the-world/')

수정을 하고 나서 단위 테스트를 진행하게 되면 아래와 같이 결과가 출력된다.

    from lists.models import Item,List
ImportError: cannot import name 'List' from 'lists.models' 

코드 수정은 다음과 같다.
lists/models.py에 해당 클래스를 추가해준다.

class List(models.Model):
    pass

이렇게 변경 후, 단위 테스트를 실행해보면 아래와 같은 오류가 발생하게 된다.

django.db.utils.OperationalError: no such table: lists_list

해당 건을 해결하기 위해 makemigrations 을 실행한다.
그 후, 단위테스트를 실행하면 다음과 같은 메시지가 출력된다.

    self.assertEqual(first_saved_item.list,list_)
AttributeError: 'Item' object has no attribute 'list'

Item에 어떻게 리스트 속성을 부여할 수 있을까?
텍스트로 속성을 부여해보도록 한다.

lists/models.py

from django.db import models

# Create your models here.
class Item(models.Model):
    text = models.TextField(default='')
    list = models.TextField(default='')

class List(models.Model):
    pass

코드를 수정하고, 마이그레이션까지 진행하면 다음과 같은 메시지가 출력된다.

    self.assertEqual(first_saved_item.list,list_)
AssertionError: 'List object (1)' != <List: List object (1)>

메시지를 유심히 보게되면 Django가 List 객체를 문자열로 해석해서 저장하고 있다.
객체 자체를 저장하려면 ForeignKey를 이용해서 두 클래스 간의 관계를 Django에게 알려줘야 한다.

lists/models.py

  • 외래키 내 List를 받기 위해서는 해당 클래스가 위에 있어야 한다.
from django.db import models

# Create your models here.
class List(models.Model):
    pass

class Item(models.Model):
    text = models.TextField(default='')
    list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)

코드 수정을 하고 마이그레이션을 한 뒤, 단위 테스트를 진행해본다.

Django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id

아래와 같이 테스트가 실패하게되었지만, 모델 테스트는 성공했다.
각 작업 아이템이 부모 목록을 가지고 있어야 한다는 Items 과 Lists rhksrP Eoansdlek.
기존 테스트가 아직 이 관계를 인식할 준비가 돼있지 않다.

ListsViewTest를 수정해서 두 개 테스트 아이템을 위한 부모 목록을 만드는 것이다.

list/tests.py

class ListViewTest(TestCase):

    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')

    def test_display_all_items(self):
        list_ = List.objects.create()
        Item.objects.create(text='itemey 1', list=list_)
        Item.objects.create(text='itemey 2', list=list_)

테스트를 진행하면 실패하는 두 개 오류가 남는다.
뷰에도 비슷한 작업을 진행해주면 테스트가 성공하게 된다.
lists/views.py

from django.shortcuts import redirect,render
from django.http import HttpResponse
from lists.models import Item,List

def home_page(request):
    return render(request,'home.html')


def view_list(request):
    items = Item.objects.all()
    return render(request,'list.html',{'items' : items})

def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'],list=list_)
    return redirect('/lists/the-only-list-in-the-world/')

각 목록이 하나의 고유 URL을 가져야 한다

ListViewTest를 수정해서 두 테스트가 새로운 URL을 가리키도록 한다.

lists/tests.py

class ListViewTest(TestCase):

    def test_uses_list_template(self):
        list_ = List.objects.create()
        response = self.client.get('/lists/%d/' % (list_.id,))
        self.assertTemplateUsed(response, 'list.html')

    def test_display_all_items(self):
        correct_list = List.objects.create()
        Item.objects.create(text='itemey 1', list=correct_list)
        Item.objects.create(text='itemey 2', list=correct_list)
        other_list = List.objects.create()
        Item.objects.create(text='다른 목록 아이템 1', list=other_list)
        Item.objects.create(text='다른 목록 아이템 2', list=other_list)
        
        response = self.client.get('/lists/%d/' % (correct_list.id))

        self.assertContains(response, 'itemey 1')
        self.assertContains(response, 'itemey 2')
        self.assertNotContains(response, '다른 목록 아이템 1')
        self.assertNotContains(response, '다른 목록 아이템 2')

아래와 같이 변경 후 단위 테스트 실행 시 404 에러와 이에 관련된 에러가 발생한다.

AssertionError: No templates used to render the response

URL을 통해 어떻게 파라미터를 전달하는지 배워보도록 한다.

일단 url을 표현하기 위해 path 와 re_path()를 사용하였다.
책에 있는대로 그대로 했는데 계속 404 에러가 발생해서 관련해서 이것저것 구글링을 좀 해봤을때는 다음과 같다.

http://pythonstudy.xyz/python/article/311-URL-%EB%A7%A4%ED%95%91

일단 path를 써서 사용할 수도있지만, 내장된 컨버터가 부족할 경우에는 re_path()를 통해서 정규 표현식으로 사용해도 된다고 한다.

이에 view_list url을 re_path로 변경해서 사용하였다.

re_path(r'lists/(.+)/$', views.view_list, name='view_list'),

이렇게 표현을 바꾸고 단위테스트를 진행하게 되면 책에서 원하는 기대결과와 같은 테스트 실패 메시지가 출력되게 된다.

TypeError: view_list() takes 1 positional argument but 2 were given

views.py 에 더미 파라미터를 적용해서 해당 문제를 해결하도록 한다.

lists/views.py

def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    items = Item.objects.filter(list=list_)
    return render(request, 'list.html', {'items': items})

뷰가 어떤 아이템을 템플릿에 보낼 지 구분하는 처리를 추가한다.

다른 테스트를 하게되면, 에러가 추가로 발생하게되는데 뷰를 확인해서 유효한 곳으로 리디렉션 하도록 수정한다.
lists/tests.py

    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text': '신규 작업 아이템'}
        )
        # self.assertEqual(response.status_code, 302)
        # self.assertEqual(response['location'],
        #                  '/lists/the-only-list-in-the-world/')
        # self.assertRedirects(response, '/lists/the-only-list-in-the-world/')
        new_list = List.objects.first()
        self.assertRedirects(response, f'/lists/{new_list.id}/')

lists/views.py

def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'], list=list_)
    # return redirect('/lists/the-only-list-in-the-world/')
    return redirect(f'/lists/{list_.id}/')

이렇게까지 변경하게되면 단위 테스트는 성공하게된다!

기능테스트를 하게되면 모든 POST 전달 시마다 새로운 목록을 만들기 때문에, 여러 아이템을 하나의 목록에 추가하는 기능 동작이 하지 않는 것을 확인할 수 있다.

기존 목록에 아이템을 추가하기 위한 또 다른 뷰

기존 목록에 신규 아이템을 추가하기 위한 URL과 뷰가 필요하다.

lists/tests.py

class NewItemTest(TestCase):
    def test_can_save_a_POST_request_to_an_Existing_list(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()

        self.client.post(
            f'lists/{correct_list.id}/add_item',
            data={'item_text': '기존 목록에 신규 아이템'}
        )

        self.assertEqual(Item.objects.count(), 1)
        new_item = Item.objects.first()
        self.assertEqual(new_item.text, '기존 목록에 신규 아이템')
        self.assertEqual(new_item.list, correct_list)

    def test_redirects_to_list_view(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()

        response = self.client.post(
            f'/lists/{correct_list.id}/add_item',
            data={'item_text': '기존 목록에 신규 아이템'}
        )

        self.assertRedirects(response, f'/lists/{correct_list.id}')

단위 테스트를 실행하게 되면 결과는 다음과 같이 나오게 된다.

    self.assertEqual(
AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)

이것도 의아한 부분인데 원래대로라면 404가 나와야하는데 왜 200이 응답될까.. 고민을 했는데
일단 URL 부분을 좀 수정해주면 원하는 값을 얻을 수 있다.
(이게 맞는지 조금 의문스러워서 조금씩 진행해보면서 다른 부분이 있는지 확인해 볼 예정이다.)
path('lists/<int:list_id>/', views.view_list, name='view_list'),

AssertionError: 404 != 302 : Response didn't redirect as expected: Response code was 404 (expected 302)

기존 목록에 신규아이템을 추가하는 URL을 만들어보도록 한다.
lists/urls.py

urlpatterns = [
    # Examples :
    # path('', views.home, name='home'),
    # path('lists/the-only-list-in-the-world/', views.view_list, name='view_list'),
    path('', views.home_page, name='home'),
    # re_path(r'lists/(.+)/', views.view_list, name='view_list'),
    # re_path(r'lists/(.+)/add_item/', views.add_item, name='add_item'),
    path('lists/<int:list_id>/', views.view_list, name='view_list'),
    path('lists/<int:list_id>/add_item', views.add_item, name='add_item'),
    path('lists/new', views.new_list, name='new_list'),
    # path('^blog/',include('blog.urls')),
    # path('^admin/', include(admin.site.urls)),
]

추가해주고 테스트를 진행해보면 다음과 같은 결과를 확인할 수 있다.

    path('lists/<int:list_id>/add_item/', views.add_item, name='add_item'),
AttributeError: module 'lists.views' has no attribute 'add_item'

views.py 에 add_item 관련 함수를 추가해준다.

lists/views.py

def add_item(request, list_id):
    pass

이렇게 수정하고 단위 테스트를 진행하면 아래와 같이 결과가 출력된다.

ValueError: The view lists.views.add_item didn't return an HttpResponse object. It returned None instead.

new_list에서 redirect를 복사하고 view_list 에서 List.object를 복사한다.

lists/views.py

def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    return redirect(f'/lists/{list_.id}/')

이렇게 수정하고 결과를 확인했을때 아래와 같이 확인된다.

AssertionError: 0 != 1

마지막으로 신규 작업 아이템을 저장할 수 있도록 한다.

def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect(f'/lists/{list_.id}/')

단위 테스트가 통과됨을 확인할 수 있다.

이제 이 URL을 list.html 템플릿에서 이용하면 된다.

templates/list.html

    <form method="POST" action="/lists/{{list.id}}/add_item">
        <input name="item_text" id="id_new_item" placeholder="작업 아이템 입력" />
        {% csrf_token %}
    </form>

lists/tests.py

class ListViewTest(TestCase):

    def test_uses_list_template(self):
        list_ = List.objects.create()
        response = self.client.get(f'/lists/{list_.id}/')
        self.assertTemplateUsed(response, 'list.html')

    def test_display_only_items_for_that_list(self):
        correct_list = List.objects.create()
        Item.objects.create(text='itemey 1', list=correct_list)
        Item.objects.create(text='itemey 2', list=correct_list)

        other_list = List.objects.create()
        Item.objects.create(text='다른 목록 아이템 1', list=other_list)
        Item.objects.create(text='다른 목록 아이템 2', list=other_list)

        response = self.client.get(f'/lists/{correct_list.id}/')

        self.assertContains(response, 'itemey 1')
        self.assertContains(response, 'itemey 2')
        self.assertNotContains(response, '다른 목록 아이템 1')
        self.assertNotContains(response, '다른 목록 아이템 2')

    def test_passes_correct_list_to_template(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()
        response = self.client.get(f'/lists/{correct_list}/')
        self.assertEqual(response.context['list'], correct_list)

결과는 다음과 같다.

KeyError: 'list'

list 를 템플릿에 전달하고 있지 않기 때문이다.
간략화할 수 있다.

lists/views.py

def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    return render(request, 'list.html', {'list': list_})

또한 list.html에 있는 폼의 POST를 수정처리하면 해결된다.

lists/list.html

<html>

<head>
    <title>To-Do lists</title>
</head>

<body>
    <h1>작업 목록 시작</h1>
    <form method="POST" action="/lists/{{list.id}}/add_item">
        <input name="item_text" id="id_new_item" placeholder="작업 아이템 입력" />
        {% csrf_token %}
    </form>
    <table id="id_list_table">
        {% for item in list.item_set.all %}
        <tr>
            <td>{{forloop.counter}}: {{item.text}}</td>
        </tr>
        {% endfor %}
    </table>
</body>

</html>

이렇게 수정하고 단위테스트와 기능테스트 진행 시 전부 성공하는 것 확인할 수 있다.

URL includes를 이용한 마지막 리팩터링

전체 사이트에 적용할 URL을 설정한다.

superlists/urls.py

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/', include('lists.urls')),
    # path('lists/<int:list_id>/', views.view_list, name='view_list'),
    # path('lists/<int:list_id>/add_item', views.add_item, name='add_item'),
    path('lists/new', views.new_list, name='new_list'),
    # path('^blog/',include('blog.urls')),
    # path('^admin/', include(admin.site.urls)),
]

lists/urls.py

urlpatterns = [
    path('<int:list_id>/', views.view_list, name='view_list'),
    path('<int:list_id>/add_item', views.add_item, name='add_item'),
    path('new', views.new_list, name='new_list'),
    # path('^admin/', include(admin.site.urls)),
]

단위/기능 테스트 전부 통과하는 것 확인하였다.
길고 탈도 많은 6장이 드디어 막을 내렸다.


마무리

1) TypeError: view must be a callable or a list/tuple in the case of include().

URL을 변경하고 테스트를 실행하면 다음과 같은 메시지를 볼 수 있다.
현재 도서에 작성되어있는대로 사용하지 않기때문이다.

이를 해결하기위해서는 어떻게 해야할까?

https://velog.io/@woonmong/TDD-3.-Simple-homepage-testing-using-unit-tests

해당 페이지에서 한번 수정을 한 적이 있는데 이번에도 동일하게 맞춰서 사용하면 된다.

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/the-only-list-in-the-world/$', views.view_list, name='view_list'),

]

2) ?: (2_0.W001) Your URL pattern 'lists/the-only-list-in-the-world/' [name='view_list'] has a route that contains '(?P<', begins with a '^', or ends with a ''. This was likely an oversight when migrating to django.urls.path().

https://stackoverflow.com/questions/47661536/django-2-0-path-error-2-0-w001-has-a-route-that-contains-p-begins-wit

url 뒤에 "$" 을 지워주니 warning 이 사라졌다.

    path('lists/the-only-list-in-the-world/', views.view_list, name='view_list'),
    path('', views.home_page, name='home'),

3) AssertionError: No templates used to render the response

list.html을 만들고 테스트를 실행하는 과정해서 아래의 오류가 계속 발생하였다.

구글링하면서 이것저것 찾아보니 내가 하나를 안썼다.
주소 앞에 "/" 추가하니 정상적으로 진행되는 것 확인하였다.

https://stackoverflow.com/questions/42685637/check-for-template-fails-because-no-templates-used-to-render-the-response

    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')

4) 도서에 대한 특이점

번역이 된듯 만듯 한 느낌이 든다.
p106 에서 "작업 목록 시작"으로 변경했을때, 도서에서는 크게 변경점에 대해서 특이사항이 없었으나 실제적으로는 오류가 발생했다.

AssertionError: 'To-Do' not found in '작업 목록 시작'

도서와 똑같은 결과를 얻고 싶다면 아래와 같이 해당 부분도 수정해줘야한다.
일단 아이템 리스트를 확인하기 전에, 해당 부분이 통과가 되어야 하지 않겠는가?

실제로 기능 테스트를 진행했을때도 특이사항 없이 잘 넘어가는 것을 확인했다.

    def test_can_start_a_list_and_retrieve_it_later(self):
        # 에디스(Edith)는 멋진 작업 목록 온라인 앱이 나왔다는 소식을 듣고
        # 해당 웹 사이트를 확인하러 간다.
        # self.browser.get('http://localhost:8000')
        self.browser.get(self.live_server_url)

        # 웹 페이지 타이틀과 헤더가 'To-Do'를 표시하고 있다.
        self.assertIn('To-Do', self.browser.title)
        header_text = self.browser.find_elements(By.TAG_NAME, 'h1')
        for i in header_text:
            self.assertIn('작업 목록 시작', i.text)

5) self.assertRegex(francis_list_url,'/lists/.+')

AssertionError: Regex didn't match: '/lists/.+' not found in 'http://localhost:59400/'

관련하여 구글링을 해봤더니,구문이 실행되어야 하는데 URL로 이동하기 전에 구문이 실행되어 오류가 발생하는 것으로 확인했다.
https://ksy37667.tistory.com/31

해당 구문에도 time.sleep(1)을 추가해주도록 한다.

functional_tests/tests.py

        # 프란시스가 새로운 작업 아이템을 입력하기 시작한다
        # 그는 에디스보다 재미가 없다
        inputbox = self.browser.find_element(By.ID,'id_new_item')
        inputbox.send_keys('우유 사기')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        # 프란시스가 전용 URL을 취득한다.
        francis_list_url = self.browser.current_url
        self.assertRegex(francis_list_url,'/lists/.+')
        self.assertNotEqual(francis_list_url,edith_list_url)

결과가 정상적으로 출력되는 것을 확인할 수 있다.

6) TypeError: init() missing 1 required positional argument: 'on_delete'

Django 2.0 버전 이후부터는 2개의 파라미터를 입력받도록 되어있습니다.
따라서 두번째 파라미터로 on_delete 시 수행하게 되는 기능을 추가해서 해결이 가능합니다.

https://stackoverflow.com/questions/44026548/getting-typeerror-init-missing-1-required-positional-argument-on-delete

lists/models.py

from django.db import models

# Create your models here.
class List(models.Model):
    pass

class Item(models.Model):
    text = models.TextField(default='')
    list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)

7) AssertionError: 404 != 200 : Couldn't retrieve redirection page '/lists/lists/1/': response code was 404 (expected 200)

책에서 하라는 대로 똑같이 했는데, 계속 redirect에서 lists/1인 것을 확인했다.
이것 때문에 계속 값이 같지가 않아서 테스트에 실패를 하고 있는 것을 발견했다.

일단 책은 잠시 접어두고, 관련해서 해당 오류를 어떻게 해결할 건지 하나씩 짚어가기 시작했다.

첫 번째로 봐야할 것은, 해당 에러가 발생을 하는 곳인데 TraceBack을 살펴보니
FAIL: test_redirects_after_POST (lists.tests.NewListTest)

여기에서 테스트 실패를 하는 것을 발견했다.
겨우 찾았다.

views.py 의 new_list에서 redirect할때 앞에 /를 빼먹었다.
와.. 이거 하나 해결하려고 1시간을 삽질했다..

def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect(f'/lists/{list_.id}/')

8) AssertionError: '2: 공작깃털을 이용해서 그물 만들기' not found in ['1: 공작깃털을 이용해서 그물 만들기']

기능 테스트를 했을때 정상적인 결과는
2: 공작깃털 이용해서 그물 만들기 not found in 1. 공작깃털 사기여야 하는데,
자꾸 1. 공작깃털 이용해서 그물 만들기가 나오고 있었다.

실제로 기능테스트를 실행해보면 1. 공작깃털 사기 값이 잘 들어가는데,
두번째 입력할때 1. 공작깃털 이용해서 그물 만들기로 들어가는 것을 확인했다.

이부분은 아무리 찾아도 잘 모르겠어서 조금 더 진행해보고 확인해 볼 예정이다.
(일단 맞는 상태이기는 하다. 현재 상태에서)




참고 도서 : 클린 코드를 위한 테스트 주도 개발 (비제이퍼블릭,2015)

profile
#QA #woonmong

0개의 댓글