본문 바로가기

프로그래밍/language

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

해당 포스트는 Django Framework에 대한 기초적인 지식을 필요로 한다.

아래의 링크를 통해 튜토리얼의 소스코드를 clone 할 수 있다.

https://github.com/deadlylaid/testing

 

1부에서 pytest와 django를 이용한 간단한 테스트 코드를 작성해 보았다.

2부에서는 pytest fixture에 대해 알아본다.

 

pytest가 데이터베이스에 접근하기 위해서 markers를 사용했다. 하지만 테스트가 많아질수록 점점 더 많은 markers를 사용해야 할 것이고 나중에는 자칫 코드가 난해해 보일 수도 있다. 게다가 1부에서도 언급했듯 이는 여러 방법 중 하나일 뿐이다.

 

 

문서는 markers의 사용을 권장하지만, 이것이 유일한 방법은 아니다.

 

markers 없이 데이터베이스에 접근하는 또 하나의 방법은 fixture를 사용하는 것이다. pytest-django에는 db라는 fixture가 존재하는데, 일반적으로 fixture에서 데이터베이스 설정을 위해 사용하지만, 이를 응용하면 재미있는 기능을 만들어낼 수 있다.

 

@pytest.fixture(scope="function")
def access_db(db):
    pass
    
def test_masterpiece_listview(access_db):
    obj = Masterpiece.objects.create(
        name='Creation of Adam',
        author='Michelangelo',
        price='100.99'
    )
    assert obj

 

fixture에 키워드 매개변수로 사용된 scope는 해당 fixture가 설정을 공유할 범위를 말한다. 기본값은 function이며 이는 각 함수 별로 설정을 새로 하는 것을 의미한다. 이 함수를 매개변수로 활용하면 markers 없이 데이터베이스에 접근할 수 있다.

 

다음으로 하게 될 작업은 listview 테스트의 계속이다. 이전에 listview를 테스트할 때는 단순히 응답 상태 200을 반환하는지 여부만 확인했다. 하지만 이는 올바른 테스트 방법이 아니다. 최소한 객체 리스트가 제대로 반환되는지 정도는 확인해 보아야한다. 이를 위해서는 fixutre를 이용해 테스트를 위해 사용될 가짜 테이터를 만들어야한다. test_masterpiece_listview 함수를 활용해보자

 

@pytest.fixture(scope='module')
def create_obj(access_db):
    Masterpiece.objects.create(
        name='Creation of Adam',
        author='Michelangelo',
        price='100.99'
    )
    
 def test_listview_get_test(client, access_db, create_obj):
    resp = client.get(
        reverse('list')
    )
    assert resp.context_data['object_list'][0].name == 'Creation of Adam'
    assert resp.status_code == 200

 

기존에 작성해 두었던 test_masterpiece_listview 함수를 fixture로 변경하여 데이터베이스 초기화 용도로 사용하였다. 이 초기화 작업은 테스트를 처음 실행할 때 한 번만 수행할 것이기 때문에 module 단위 scope를 설정한다. 이 커스텀된 fixture를 데이터베이스를 초기화 하고 listview에 반환된 오브젝트를 테스트해보자.

 

테스트를 실행하면 아래의 에러가 발생하는데 이는 fixture를 만들 때 정의해준 scope가 원인이다. create_obj 를 만들때 사용된 db fixture가 이미 function scope이기 때문에 커스텀 module scope 이용하면 두 fixture의 scope 범위에 충돌이 에러가 발생한다.

 

db는 기본적으로 function scope를 갖고 있다.

 

위의 에러를 해결하기 위해서 두 가지 방법을 생각해보았다.

  • module 단위 scope를 포기하고 db fixture를 따라간다.
  • db fixture를 포기하고 module 단위 scope를 지원하는 것을 찾아본다.

상황에 따라 다르게 선택하겠지만, 아직까지는 데이터베이스 초기화를 테스트 함수마다 실행할 필요는 없어보여서 module 단위 scope를 고집하기로 한다. 다른 방법이 찾아보았는데 django_db_blocker 라는 fixture를 찾게 되었다.

 

@pytest.fixture(scope='module')
def create_obj(django_db_blocker):
    with django_db_blocker.unblock():
        Masterpiece.objects.create(
            name='Creation of Adam',
            author='Michelangelo',
            price='100.99'
        )

 

위와 같이 unblock 함수를 사용해서 데이터베이스의 초기값을 설정할 수 있다.

django_db_blocker 에서 unblock 함수를 사용할 경우, 데이터베이스에 접근이 가능하도록 '열려진' 상태가 된다. 그렇기 때문에 테스트 코드에서 데이터베이스 정보를 수정할 수 있기 때문에 자칫 테스트 코드를 실행하다가 초기화 데이터를 변형시킬 수 있다.
이런 상황은 다른 테스트에 영향을 미칠 수 있기 때문에 주의가 필요하다.

 

몇 가지 기본적인 fixture를 이용하여 테스트 코드를 리펙토링하였는데, 많은 fixture 함수와 테스트 함수가 한 데 뒤섞여 있으니 어딘가 난해한 느낌이 든다. 이 문제는 conftext.py를 사용하여 해결 할 수 있다. conftest.py는 각 테스트 디렉토리 별로 위치할 수 있는데, 해당 디렉토리에 존재하는 테스트 코드들이 필요로 하는 여러가지 설정값들을 넣는다.

 

## conftest.py

@pytest.fixture
def access_db(db):
    pass


@pytest.fixture(scope='module')
def create_obj(django_db_blocker):
    with django_db_blocker.unblock():
        Masterpiece.objects.create(
            name='Creation of Adam',
            author='Michelangelo',
            price='100.99'
        )

이렇듯 설정값의 격리를 통해서 테스트 코드를 좀 더 간결하게 작성할 수 있다.

 

pytest에는 위에 나온 기능 뿐 아니라 정말 다양한 fixture가 존재하며 이를 적절히 사용하면 정말 효율적으로 테스트 코드를 작성할 수 있다.