기능 테스트 | 단위 테스트 | |
---|---|---|
관점 | 사용자 관점 | 프로그래머 관점 |
목표 | 애플리케이션 외부 | 애플리케이션 내부 |
레이어 | 상위 레벨 | 하위 레벨 |
목적 | 제대로 된 기능성을 갖춘 애플리케이션을 구축 | 깔끔하고 버그없는 코드를 작성 |
해당 명령어를 통해 작업 목록 앱을 생성해준다.
python3 manage.py startapp lists
superlists/superlists 와 같은 위치에 superlists/lists라는 폴더가 생성된다.
lists내의 test.py 파일을 열어 코드를 수정한다.
from django.test import TestCase
# Create your tests here.
class SmokeTest(TestCase):
def test_bad_maths(self):
self.assertEqual(1+1,3)
해당 코드를 실행해주면 아래와 같이 결과가 나온다.
$ python3 manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_bad_maths (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/lists/tests.py", line 9, in test_bad_maths
self.assertEqual(1+1,3)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
이렇게 결과값이 나온다면 정상적인 기대결과값을 얻은 것이다.
Django 는 대체로 모델-뷰-컨트롤러(Model-View-Controller, MVC)라는 고전적인 패턴을 따른다.
주요 역할은 일반적인 웹 서버처럼 사용자가 특정 URL을 요청했을때 어떤 처리를 할지 결정하는 것이다.
Django의 처리 흐름은 다음과 같다.
이에 우리가 테스트 해야할 것은 2가지다.
여기서 View 함수가 무엇을 의미하냐면,
View는 필요한 데이타를 모델 (혹은 외부)에서 가져와서 적절히 가공하여 웹 페이지 결과를 만들도록 컨트롤하는 역활을 한다.
참고 자료 : http://pythonstudy.xyz/python/article/306-Django-%EB%B7%B0-View
lists/test.py
from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
# Create your tests here.
# class SmokeTest(TestCase):
# def test_bad_maths(self):
# self.assertEqual(1+1,3)
class HomePageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
책에 써있는 코드와 조금 다르게 작성하였는데, 이는 아래 마무리 단계를 참고하면 될 것 같다.
해당 코드를 실행하면 에러가 발생하게 된다.
이유는 아직 존재하지 않는 무언가를 import 하려고 했기 때문이다.
실패 테스트를 진행해봤으니 이를 해결하기 위한 최소한의 수정만 해보도록 한다.
현재 윗 상황에서 실패한 이유는 lists.views 에서 home_page를 import 할 수 없는 상태이기 때문이다.
이에 views.py의 코드를 아래와 같이 수정해주었다.
from django.shortcuts import render
# Create your views here.
home_page = None
코드 수정 후, 해당 코드를 실행해보면 아래와 같이 결과가 나온다.
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/lists/tests.py", line 13, in test_root_url_resolves_to_home_page_view
found = resolve('/')
File "python3.9/site-packages/django/urls/base.py", line 24, in resolve
return get_resolver(urlconf).resolve(path)
File "/python3.9/site-packages/django/urls/resolvers.py", line 683, in resolve
raise Resolver404({"tried": tried, "path": new_path})
django.urls.exceptions.Resolver404: {'tried': [[<URLResolver <URLPattern list> (admin:admin) 'admin/'>]], 'path': ''}
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
Destroying test database for alias 'default'...
해당 TraceBack을 살펴보면 아래와 같은 내용을 확인할 수 있다.
1. 어떤 테스트가 실패하고 있는가? (현재는 예측된 실패)
2. 어떤 에러가 발생했는가?
3. 어느 부분에서 에러가 발생했는가? (어느 코드에서 에러가 발생했는지)
해당 오류는 "/" 확인 시, Django가 404에러를 발생시켜서 발생하는 오류이다.
해당 문제를 해결해보자.
url.py 코드에 아래의 내용을 추가한다.
from django.contrib import admin
from django.urls import path, include
from lists import views
urlpatterns = [
# Examples :
path('', views.home_page, name='home'),
# path('^blog/',include('blog.urls')),
# path('^admin/', include(admin.site.urls)),
]
해당 코드로 단위 테스트를 다시 실행해보면 아래와 같이 결과가 나온다.
File "/superlists/urls.py", line 26, in <module>
path('', views.home, name='home'),
AttributeError: module 'lists.views' has no attribute 'home'
이를 미루어 볼 때, lists.views.home이 존재하지 않는다고 이해가 된다.
해당 객체를 home_page를 가리키게 변경한다.
path('', views.home_page, name='home'),
raise TypeError(
TypeError: view must be a callable or a list/tuple in the case of include().
변경했을때에도, home_page를 호출할 수 없다는 메시지가 뜨고있으나 이전과는 다르다.
home_page가 아직 함수가 아니기에 이와 같은 오류가 발생하므로 이전에 작성한 Home_page = None
부분을 실제 함수로 변경해본다.
from django.shortcuts import render
# Create your views here.
# home_page = None
def home_page():
pass
변경하고 테스트 실행 시, 첫 단위 테스트가 성공하게 된다.
$ python3 manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Destroying test database for alias 'default'...
뷰를 위한 테스트를 작성할 때는 단순히 빈 함수를 작성하는 것이 아닌 HTML 형식의 실제 응답을 반환하는 함수를 작성해야 한다.
test.py 내 해당 테스트를 추가한다.
lists/tests.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
class HomePageTest(TestCase):
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)
self.assertTrue(response.content.startswith(b'<html>'))
self.assertIn(b'<title>To-Do lists</title>', response.content)
self.assertTrue(response.content.endswith(b'</html>'))
단위 테스트를 실행해서 결과를 확인해보면 아래와 같다.
Found 2 test(s).
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 "lists/tests.py", line 20, in test_home_page_returns_correct_html
response = home_page(request)
TypeError: home_page() takes 0 positional arguments but 1 was given
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=1)
Destroying test database for alias 'default'...
코드의 품질을 높이고 싶으면 코드 변경을 최소화해야한다.
이렇게 최소화한 코드는 하나하나 테스트에 의해 검증되어야 한다.
자신이 있어도 작은 단위로 나누어 코드를 변경해야한다.
예시) 최소한의 코드변경 - 테스트 반복
1) list/views.py
def home_page(request):
pass
2) 1번 내용 테스트
AttributeError: 'NoneType' object has no attribute 'content'
3) django.http.httpRespnse 사용
from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
# home_page = None
def home_page(request):
return HttpResponse()
4) 3번 내용 테스트
Traceback (most recent call last):
File "/lists/tests.py", line 21, in test_home_page_returns_correct_html
self.assertTrue(response.content.startswith(b'<html>'))
AssertionError: False is not true
5) response 값 추가
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title>')
6) 5번 내용 테스트
Traceback (most recent call last):
File "lists/tests.py", line 23, in test_home_page_returns_correct_html
self.assertTrue(response.content.endswith(b'</html>'))
AssertionError: False is not true
7) 마지막 테스트
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title></html>')
8) 7번 내용 테스트 (성공!)
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
해당 테스트가 끝나고 난 후, 단위 테스트를 진행해본다.
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/woonmong/python/wm-tdd/tdd_project/django_project/superlists/functional_test.py", line 21, in test_can_start_a_list_and_retrieve_it_later
self.fail('Finish the test!')
AssertionError: Finish the test!
----------------------------------------------------------------------
Ran 1 test in 3.393s
FAILED (failures=1)
여기서는 실패는 작업 완료 메시지를 출력하기 위해 심어둔 AssertionError 때문이고, 성공한 것이다.
드디어 웹 페이지를 가지게 되었다.
3일차, 3장의 내용도 커밋 후 복습을 해본다.
3일차 실습 내용 :
https://github.com/woonmong712/wm-tdd
진행 중 발생한 특이 사항에 대해 공유해보면 아래와 같다.
튜토리얼을 그대로 따라하던 중, 해당 구문에서 에러가 발생하였다.
from django.core.urlresolvers import resolve
확인해보니, 해당 문제는 Django 버전 문제로, Django 2.0에서는 django.core.urlresolvers 모듈을 삭제했기 때문에 발생하는 오류였다.
코드를 아래와 같이 바꿔주면 정상적으로 사용할 수 있다.
from django.urls import resolve
Django 1.8 이후로는 patterns 가 없어졌다고 한다.
As of Django 1.10, the patterns module has been removed (it had been deprecated since 1.8).
그냥 이렇게 사용하면 된다.
urlpatterns = [
# Examplets:
url(r'^$','superlists.views.home', name='home'),
# url(r'^blog/, include('blog.urls')),
#url(r'^admin/', include(admin.site.urls)),
]
이것도 마찬가지로 Django == 4.0 버전부터 없어졌다고 한다.
https://forum.djangoproject.com/t/django-4-0-url-import-error/11065
url 대신 path 로 바꿔서 사용하면 된다.
from django.urls import path, include
# urlpatterns = [
# path('admin/', admin.site.urls),
# ]
urlpatterns = [
# Examples :
path(r'^$', 'superlists.views.home', name='home'),
# path(r'^blog/',include('blog.urls')),
# path(r'^admin/', include(admin.site.urls)),
]
코드 변경 및 기대 결과 확인
책이 이전버전이여서, 현재 버전에 맞게 코드를 수정해주었다.
수정해준 다음에 진행했을때 드디어 정상적으로 원하는 기대결과를 얻을 수 있었다.
from django.contrib import admin
from django.urls import path, include
from lists import views
# urlpatterns = [
# path('admin/', admin.site.urls),
# ]
urlpatterns = [
# Examples :
path('', views.home_page, name='home'),
# path('^blog/',include('blog.urls')),
# path('^admin/', include(admin.site.urls)),
]
성공 결과 :
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...