본문 바로가기

프로그래밍/language

단위 테스트 pytest-django 튜토리얼 - [3]

해당 포스트는 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를 사용하는 방법이다. 

 

외부 api에 의해 테스트 결과가 매번 달라진다면 테스트의 신뢰도는 크게 하락하게 될 것이다.

 

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_respresult 를 변수로 사용하고 그 변수에 저장될 3개의 데이터 셋이 리스트로 정의되어있다. 이 데이터 셋의 갯수만큼 test_buyer_list_view 테스트가 반복적으로 실행되며 다른 조건으로 테스트를 실행할 수 있게 된다.