만들고 있는 소프트웨어나 프로그램이 커져갈수록 전체의 프로그램을 일일히 테스트하기는 힘들 것이다. 프로그램의 구성 요소들간의 관계도 복잡해지면서 한줄의 코드를 바꾼다면 여러개의 구성 요소들도 영향을 받기 때문에 모든게 완벽하게 돌아갈 것이라고 확신하기 힘들어진다.
테스트의 종류에는 3가지가 있다.
- unit test: 함수, 클래스처럼 작은 구성 요소들을 테스트
- integration test: 서로 다른 시스템들의 상호작용이 잘 이루어지는지 테스트
- system test: 유저 입장에서 End to End 테스트
하지만 코드가 변할때마다 매번 integration test나 system test를 실행한다면 시간도 오래 걸리고 비효율적일 것이다. 이러한 어려움을 극복하기 위한 일환으로 자동화된 테스트들을 만들 수 있다. 이것이 바로 unittest
의 시작이다.
Unit test는 클래스, 함수 단위의 개별적인 구성 요소를 테스트하는 것이다.
Django는 Python의 unittest
라이브러리를 기반으로 한 테스트 프레임워크를 제공한다.
tests.py가 없이는, 백엔드는 프론트에서 httpie 요청을 받지 않고는 자신의 views.py를 테스트하기 힘들다. 터미널에서 직접 http 요청을 보내봐야하기 때문이다.
하지만, Client와 함께라면 서버가 실행되지 않고 있는 상황에서도 프론트의 요청 없이 백엔드가 직접 views.py를 실험할 수 있다.
Python class인 test client는 views를 테스트하도록 도와주는 가짜 웹 브라우저로 작동한다. GET, POST 리퀘스트를 보내줄 수 있고, redirect나 status code도 확인할 수 있다.
이를 Client()라는 클래스를 import하는 것으로 시작할 수 있다.
다음은 python manage.py shell
로 쉘을 켜서 실험해본 결과이다.
>>> from django.test import Client [1]
>>> c = Client() [2]
>>> response = c.get('/accommodation/1') [3]
>>> response.content [4]
b'{"id": 1, "title": ....
>>> type(response)
<class 'django.http.response.JsonResponse'> [5]
>>> response.status_code [6]
200
[1] django.test에서 Client를 import해온다.
[2] c 라는 변수에 Client 클래스의 인스턴스를 담는다. (인스턴스화)
[3] response라는 변수에 JsonResponse를 담는다. [5]를 보면 response의 타입을 볼 수 있다. 역시 JsonResponse 이다.
[4] response의 content에도 접근할 수 있다. 나와야하는 객체들이 bytes 타입으로 담겨있다.
[6] response의 status_code에 접근할 수 있다.
>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7})
위의 요청은 GET request를 아래의 주소에 보내는 것과 동일하다.
/customers/details/?name=fred&age=7
Accommodation class의 attribute에는 title, address만 있다고 가정하자. accommodation/1 에 http GET 요청을 보내는 것을 테스트하는 간단한 tests.py를 짜본다고 가정해보면 아래와 같을 것이다.
# tests.py
import unittest
from django.test import Client, TestCase
from accommodation.models import Accommodation
c = Client()
class MyTestCase(TestCase):
def setUp(self): [1]
Accommodation.objects.create(
title = '선릉역 좋은 집',
address = '서울특별시 강남구 테헤란로'
)
def test_accommodation_detail_get_success(self): [2]
response = c.get('/accommodation/1')
self.assertEqual(response.json(), 기대하는 값)
def tearDown(self): [3]
Accommodation.objects.all().delete()
[1] setUp 메소드에서 테스트 db에 테스트 데이터를 만든다.
[2] 원하는 테스트를 진행한다. self.assertEqual을 써서 비교할 값, 기대하는 값을 비교한다. 둘이 다르다면 Fail한다.
[3] tearDown 메소드에서 테스트 db에 만든 데이터를 없앤다.
위와 같이 setUp, tearDown을 쓰는 것도 좋지만, 아래와 같이 setUpTestData라는 것을 쓸 수도 있다! 더 효율적이고 빠르기 때문이다.
Django 공식 문서 : testing tools에 따르면...:
classmethod TestCase.setUpTestData()
This technique allows for faster tests as compared to using setUp().
- setUp : test (method 마다) 할때마다 불려서 test 데이터셋을 만들어준다.
- setUpTestData: TestCase 한개마다 (class 마다) 초기 데이터 test 데이터를 만들어준다.
# tests.py
import unittest
from django.test import Client, TestCase
from accommodation.models import Accommodation
c = Client()
class MyTestCase(TestCase):
@classmethod [1]
def setUpTestData(cls): [2]
Accommodation.objects.create(
title = '선릉역 좋은 집',
address = '서울특별시 강남구 테헤란로'
)
def test_accommodation_detail_get_success(self):
response = c.get('/accommodation/1')
self.assertEqual(response.json(), 기대하는 값)
[1] @classmethod 라는 데코레이터를 써서 클래스 메소드화 한다.
(파보려다가 멘토님들이 지금 이해하기 어려운 내용이니 패스하라고 하셨다.)
[2] setUpTestData에서 TestCase(클래스) 단위의 초기 데이터를 셋업한다.
setUp, tearDown을 쓸때보다 setUpTestData할때 시간이 훨씬 적게 걸리므로 실제로 테스트해보는 것을 권장한다!
일부러 tests.py에 잘못된 기대값을 넣어서 실패를 시켜보았다.
accommodation/tests.py 를 실행시 실패했음을 알 수 있다
위의 결과를 보면 AssertionError: Lists differ: [] != ['1']
를 볼 수 있다. 왼쪽이 실제 결과값이고 오른쪽이 기대했던 결과 값이다.
그 아래줄들에는 - [], + ['1']
이 있다. 이것은 기대했던 []
는 없고, ['1']
가 있다는 뜻이다.