Search

PyTest와 함께 사용하기 좋은 라이브러리와 플러그인

Intro

Django, Django RestFramework, PyTest에 대한 기본적인 이해가 필요한 글입니다.

PyTest Django

Django Rest Framework을 사용하는 백엔드의 테스트 코드를 짤 때 API Response를 확인하는 작업은 매우 번거롭습니다.
먼저 응답의 구조를 확인 해야하고, 각 필드의 값과 데이터 타입을 검증해야 합니다.
또 nullable한 필드도 확인해야 합니다.

1. Schema

Schema is a library for validating Python data structures — schema
Schema를 이용하면 API Response의 구조와 값 그리고 데이터 타입을 쉽게 검증할 수 있습니다.
이를 테스트 코드에 적용하여 깔끔하고 효율적인 테스트 코드를 작성 해보겠습니다.

Basic Usage

유저의 정보를 List로 반환하는 API가 있다고 해봅시다.
response = [ { "name": "회원1", "age": 20, "gender": "M" }, { "name": "회원2", "age": 20, "gender": "F" }, { "name": "비회원", "age": 18, "gender": None }, ]
Python
복사
위의 API를 검증하기 위해 schema를 이용하여 검증 할 응답의 구조를 다음과 같이 작성합니다:
schema = Schema( [ { 'name': And(str, len), 'age': And(Use(int), lambda n: 18 <= n <= 99), 'gender': Or( And(str, Use(str.upper), lambda s: s in ('M', 'F')), None ) } ] )
Python
복사
Schema를 생성하는 방법은 어렵지 않습니다.
먼저 []는 List를 검증하고, {}는 Dict를 검증합니다.
응답이 JSON array로 반환 될 것이기 때문에 가장 먼저 []를 통해 이를 표현해줍니다.
Dict의 경우, {key:value} 형태로 검증할 {필드의 이름:값 또는 함수}를 지정하면 됩니다.

name

name은 And(str, len)로 지정되어 있습니다.
And는 인자로 전달되는 type, callable, value 모두 True임을 검증하는 클래스입니다.
값이 int, str, object와 같은 타입으로 지정되어 있으면, 주어진 데이터가 해당 타입인지 확인합니다.
만약 해당 타입이 아니라면 SchemaError를 발생시킵니다.
함수, 클래스 또는 __call__를 포함한 오브젝트와 같은 callable을 만나면, 해당 callable을 호출하여 True를 반환하는지 확인합니다.
따라서 'name': And(str, len)의 경우
1.
name이 str인지 확인하고
2.
len(name)을 호출하여 name이 빈 문자열이 아닌지를 확인한 뒤
3.
두 반환값이 모두 True인 경우에 name이 유효하다고 판단합니다.
다시 말해, name이라는 키의 값은 반드시 문자열(str)이어야 하고, 빈 문자열('')은 허용하지 않습니다.
>>> len('') 0 # False
Python
복사

age

age의 경우 'age': And(Use(int), lambda n: 18 <= n <= 99)로 되어있습니다.
Use를 이용하면 값을 검증하는 과정에서 type converting이 가능합니다.
예를 들어 age가 '20'과 같이 str으로 반환된 경우 int('20')처럼 integer로 변환하여 사용합니다.
이어지는 lambda 함수는 age의 값이 특정 범위 안에 존재하는지 확인합니다.
name과 마찬가지로 And로 묶여있기 때문에 두 반환값이 모두 True를 반환하는 경우, 검증이 통과합니다.

gender

마지막으로 gender를 보겠습니다.
Or 클래스는 데이터가 하나 이상의 값 또는 타입을 허용하는 경우에 사용할 수 있습니다.
예시에는 Or 안에 None을 포함하고 있기 때문에 gender는 nullable한 필드라고 이해하면 됩니다.
And 클래스 안 쪽에는 str.upper 함수를 이용하여 모든 문자를 대문자로 바꾸어서 사용합니다.
즉 해당 값을 case-insensitive하게 검증해야 할 때 사용할 수 있습니다.
이어지는 lambda 함수를 보면 gender가 특정 enumeration 안에 포함되는 값인지 검증 합니다.

nested schema

schema 안에 값으로 다시 schema instance를 사용하면 중첩된 schema를 만들 수 있습니다.
아래와 반복되는 형태의 schema를 재사용 할 수 있습니다.
person = Schema( { 'name': str, 'age': int, } ) person_list = Schema( [ { 'id': int, 'person': person, } ] )
Python
복사

validation

최종적으로 생성한 schema를 이용하여 다음과 같이 데이터의 유효성을 검증합니다.
>> schema.is_valid(data) True
Python
복사
validate() 메소드를 이용하면 검증된 데이터가 반환됩니다.
>> validated_data = schema.validate(data)
Python
복사

Example

schema를 적용한 간단한 테스트 코드 예시를 만들어 보겠습니다.
유저의 상세 프로필을 보여주는 API의 테스트 코드입니다.
from django.shortcuts import resolve_url from .schemas import user_profile_schema def test_get_user_profile(client, user): url = resolve_url('user_profile') client.force_authenticate(user=user) response = client.get(url) schema = Schema( { 'age': int, 'mobile': str, } ) assert response.status_code == 200 assert schema.is_valid(response.json())
Python
복사

Pros & Cons

Schema를 사용하면 다음과 같은 장단점이 있습니다.

Pros

1.
ideal for TDD: TDD를 적용하기 용이하다.
a.
schema를 이용하면 테스트 주도 개발을 하기 쉽습니다.
b.
TDD에서 요구조건을 테스트 코드로 먼저 풀어내는 과정이 필요한데 schema를 이용하면 응답 형태와 validation의 요구 조건을 쉽게 코드로 표현할 수 있어 TDD에 매우 적합합니다.
2.
Less Code: 테스트 코드량이 감소한다.
a.
Response의 값을 확인하는 수많은 assert문 대신 schema를 검증하는 하나의 assert 문으로 모든 검증이 처리됩니다.
3.
Readability: 테스트 가독성이 올라간다.
a.
다른 개발자가 작성한 코드의 경우 테스트 코드를 읽으며 코드 파악을 하기도 하는데 이 때 schema를 보면 응답 형태를 미리 예상할 수 있습니다.
b.
또 어떤 validation이 필요한지 코드 안에 표현되어 있습니다.

Cons

1.
Debugging: 디버깅이 번거롭다.
a.
is_valid() 함수는 True/False로 검증 결과를 반환하기 때문에 어떤 값이 검증에 실패했는지 확인하기 위해서 디버깅시 validate()를 호출하며 테스트 코드를 작성해야합니다.
fixture: 테스트에 사용하기 위해 미리 선언된 데이터
PyTest에는 parameterize라는 유용한 기능이 있습니다.
테스트에 사용할 인자를 미리 묶어서 다양한 경우에 대해 코드 반복 없이 테스트를 실행할 수 있게 해줍니다.
pytest-xdist와 함께 사용하면 여러가지 상황에 대응하는 테스트를 빠르게 동작시킬 수 있습니다.
parameterize를 활용한 예시를 보겠습니다.
import pytest @pytest.mark.parametrize('test_input, expected',[ ('3+5', 8), ('2+4', 6), ('6*9', 42), # fail ]) def test_eval(test_input, expected): assert eval(test_input) == expected
Python
복사
세 가지 경우를 필요한 값만 바꿔가며 하나의 테스트 코드로 검증합니다.
하지만 테스트 코드를 만들다보면 위의 예시처럼 간단한 literal을 전달하는 것으로는 충분하지 않습니다.
만약 다음과 같이 미리 만든 fixture를 테스트에 parameterize로 전달하여 사용할 수 있다면 얼마나 좋을까요?
# conftest.py import pytest @pytest.fixture def test_user(db): return User.objects.create_user(name='test')
Python
복사
pytest-lazy-fixture라는 PyTest 플러그 인을 사용하면 가능합니다.
간단한 예시를 통해 사용법을 알아보겠습니다.

Example

Use your fixtures in @pytest.mark.parametrize — pytest-lazy-fixture
기존의 parameterize를 사용하듯이 syntax에 맞게 유저 fixture를 테스트에 인자로 전달하면 됩니다.
import pytest @pytest.mark.django_db @pytest.mark.parameterize('user, expected', [ (pytest.lazy_fixture('test_user_a'), 'A'), (pytest.lazy_fixture('test_user_b'), 'B'), ]) def test_get_user_name(user, expected): assert user.name == expected
Python
복사

References