해당 포스트는 Django Framework 에 대한 기초적인 지식을 필요로한다.
아래의 링크를 통해 튜토리얼의 소스코드를 clone 할 수 있다.
https://github.com/deadlylaid/testing
1부에서 pytest와 django를 이용한 간단한 테스트 코드를 작성해 보았다.
2부에서 pytest fixture에 대해 알아보았다.
3부에서는 mock의 기본 개념과 pytest mark의 종류 중 하나인 parametrize에 대해 알아본다.
Masterpiece모델을 만들어 역사적 걸작들의 데이터를 담는 작은 어플리케이션을 만들었다. 이번엔 이 걸작들을 구매하고 싶어하는 소비자들의 데이터를 모아보고 싶다. 하지만 이 소비자 데이터는 외부 API를 호출해서 넘어온 json 데이터를 받는다고 가정하고 진행하보겠다.
(api server는 직접 만들어서 진행하는 것도 좋지만 필자의 사정 상 Datatables 문서에 있는 json data를 이용해서 만들어보겠다.)
# model
# field type에 신경쓰지 말자
class Buyer(models.Model):
name = models.CharField(max_length=50)
position = models.CharField(max_length=50)
nationality = models.CharField(max_length=30)
property = models.CharField(max_length=30)
# view
class BuyerListView(ListView):
models = Buyer
template_name = 'buyer/buyer.html'
def get_queryset(self):
resp = requests.get('https://bit.ly/2nkSGF1').content
resp = json.loads(resp)
data = resp['data']
for i in data:
gender=None
if i[0] == 'jade':
gender = 'man'
elif i[0] == 'jozz':
gender = 'woman'
name = i[0] + i[1]
if not Buyer.objects.filter(name=name).exists():
Buyer(
name=i[0] + i[1],
position=i[2],
nationality=i[3],
property=i[5],
).save()
queryset = Buyer.objects.all()
return queryset
# test
def test_buyer_list_view(client, access_db):
resp = client.get(
reverse('buyer-list')
)
assert resp.status_code == 200
assert resp.context_data['object_list'][0].name == 'AiriSatou'
로직은 매우 간단하다. API를 호출하여 구매의사를 밝힌적이 없는 Buyer라면 서비스 모델에 저장하고 ListView를 통해 노출한다.
API를 사용한다는 것만 다를 뿐 동작 방식은 masterpiece와 전혀 다를게 없기 때문에 테스트 코드 역시 같은 구조로 만들 수 있다. 하지만 이는 올바른 테스트 코드가 아니다.
위의 테스트 코드는 두 가지 문제점을 갖는다.
- 호출하는 외부 API 서버가 점검 중이거나 에러를 발생하고 있을 때, 우리 서비스에 문제가 없음에도 테스트에 실패할 수 있다.
- assert 문에서 검사되는 'AiriSatou'는 초기화된 데이터가 아닌 실제 API에서 호출된 데이터이다. 그러므로 서비스에 문제가 없음에도 테스트에 실패할 수 있다.
앞선 포스트에서 우리는 Fixture를 사용하여 테스트 데이터를 초기화하는 작업을 진행하였다. 하지만 테스트를 하다보면 실제로 테스트를 위해서 실행할 수 없는 환경을 만날 때가 있다. 지금처럼 외부 API를 호출하는 로직 등이 대표적으로 그러하다. 외부 서비스 신뢰성은 우리가 보장할 필요가 전혀 없기 때문에 테스트 절차에서 완벽하게 독립되어야한다.
이 문제를 해결하기 위해서 가장 널리 사용되는 방법은 바로 mock를 사용하는 방법이다.
mock의 기능은 매우 간단한데, 테스트의 신뢰성을 위해 실행되지 말아야할 기능들의 결과값을 미리 정의해두고 테스트할 때 기능을 실행하는 대신 정의된 결과값을 반환한다.
python에는 몇 가지 mock 라이브러리가 있다.
- unittest.mock
- mock
- pytest-mock
여러 라이브러리들이 있지만 pytest 로 기획된 포스트인 만큼 pytest-mock을 이용하여 테스트를 진행한다. pytest 에서는 request-mock 이라는 request mocking 전용 fixture가 존재한다.
def test_buyer_list_view(client, access_db, requests_mock):
requests_mock.get('https://bit.ly/2nkSGF1', text='{"data":[["Airi","Satou","Accountant","Tokyo","28th Nov 08","$162,700"]]}')
resp = client.get(
reverse('buyer-list')
)
assert resp.status_code == 200
assert resp.context_data['object_list'][0].name == 'AiriSatou'
기존에 작성해두었던 test_buyer_list_view에 requests_mock을 추가한다. get 메소드를 호출할 때 뒤에 키워드 인자로 들어가는 text는 request가 호출될 때 반환될 정의된 값을 말한다. 그러면 테스트가 실행될 때 request.get 메소드가 호출되면 requests_mock.get() 메소드가 대신 실행되면서 text에 정의되어있는 값을 리턴하여 테스트를 진행하게 될 것이다. 말 그대로 응답 결과를 초기화하는 것이다.
이제 서비스의 모든 로직을 테스트했다고 생각하고 마무리할 수도 있겠지만 우리가 간과한 사항이 존재한다. 아래의 코드를 보자
#[...]
gender=None
if i[0] == 'jade':
gender = 'man'
elif i[0] == 'jozz':
gender = 'woman'
#[...]
대부분의 서비스에는 조건문이 포함된다. 조건문은 말그대로 특정 조건에서만 실행되는 코드를 이야기하며 테스트할 때도 이 조건을 만족하는 테스트가 존재해야 테스트를 검증할 수 있다. 물론 text 의 응답 케이스에 data를 추가하는 방법이 있지만 모든 상황에서 반복문이 존재하는 것은 아니기 때문에 좀 더 활용도 높은 방법인 parametrize를 사용해 보기로 한다.
parametrize는 같은 테스트를 다른 데이터 조건으로 반복해서 실행하고자 할 때 사용한다. 일반적으로 데코레이터로 사용하며 사용법은 아래와 같다.
@pytest.mark.parametrize(
'api_resp,result', [('{"data":[["jade","Satou","Accountant","Tokyo","28th Nov 08","$162,700"]]}','jadeSatou'),('{"data":[["jozz","Satou","Accountant","Tokyo","28th Nov 08","$162,700"]]}','jozzSatou')]
)
def test_buyer_list_view(api_resp, result, client, access_db, requests_mock):
requests_mock.get('https://bit.ly/2nkSGF1', text=api_resp)
resp = client.get(
reverse('buyer-list')
)
assert resp.status_code == 200
assert resp.context_data['object_list'][0].name == result
parametrize 데코레이터는 두 개의 매개변수를 받는데 첫 번째는 초기화된 데이터가 저장될 변수이고 두 번째는 초기화 할 데이터를 저장한다. 위의 코드는 api_resp와 result 를 변수로 사용하고 그 변수에 저장될 3개의 데이터 셋이 리스트로 정의되어있다. 이 데이터 셋의 갯수만큼 test_buyer_list_view 테스트가 반복적으로 실행되며 다른 조건으로 테스트를 실행할 수 있게 된다.
'프로그래밍 > language' 카테고리의 다른 글
mysql character set error + django test (0) | 2020.05.17 |
---|---|
setup.py vs requirements.txt (0) | 2019.12.29 |
단위 테스트 pytest-django 튜토리얼 - [2] (0) | 2019.08.04 |
python 2.7은 곧 종료된다. (0) | 2019.07.06 |
단위 테스트 pytest-django 튜토리얼 - [1] (0) | 2019.06.29 |