Pytest - Django

Error Coder·2023년 6월 30일
0

Pytest

pytest-django는 django 프로젝트에서 pytest를 사용하기 위한 플러그인
pytest와 pytest-django는 Standard Django Test suite와 Nose Test suite 호환됨
단, 테스트 실행은 django의 manage.py test를 사용하지 않고 pytest 명령어를 사용
manage.py를 사용하지 않는 이유는 unittest를 임포트해서 TestCase 클래스의 서브 클래스로 선언할 필요가 없고, 단순하게 테스트 함수만 작성하는 것만으로 테스트를 작성할 수 있기 때문
그리고 Fixture를 관리할 수 있는 편리함과 pytest의 다양한 플러그인도 사용할 수 있다는 장점이 있음

Pytest 사용 이유

django에는 기본 테스트 모듈이 내재 되어 있음
그런데 왜 Pytest를 별도로 실행을 해야하는 걸까?

① django 내장 테스트는 Boiler-Plate가 존재
모든 테스트 set이 단일 클래스(django.test.TestCase)를 상속받아 운영
실행이 순차적으로 진행이 되다 보니 규모가 커지면서 느려짐
반면 pytest는 멀티 실행을 지원

② 코드 작성이 단순해짐
django
모든 테스트가 class 상속으로 이루어짐
assert를 각 class의 method로 평가해야 함
pytest
function 단위로 테스트를 작성 할 수 있음
단순 assert로 테스트를 평가할 수 있음

③ django 외 다른 파이썬 프로젝트에도 도입이 가능

④ 그 외 테스트 config를 파일로 지정해 놓을 수 있으며 fixture를 정해놓고 여러 곳에서 원하는 순서에 맞추어 실행시켜 볼 수 있음

  • Boiler-Plate(보일러플레이트)란?

최소한의 변경으로 여러 곳에서 재사용되며 반복적으로 비슷한 형태를 띄는 코드를 말함
어원: 보일러플레이트 코드의 어원은 신문사업에서 나왔음
1890년대에 광고나 컬럼과 같이 계속 사용되는 텍스트 인쇄판은 부드러운 납 대신 강철로 찍기 시작했는데 이를 Boiler-Plate라고 불렀음

설치 및 기본 설정

설치

$ pip install pytest-django

기본 설정

# pytest.ini - Project root folder
# 아래 설정을 잡아 주지 않을 경우, pytest 실행 시 에러 발생

[pytest]
DJANGO_SETTINGS_MODULE = project.settings
  • 테스트를 실행할 때 django 프로젝트의 설정을 사용하기 때문에 pytest.ini 파일에 위와 같이 명시하거나 --ds=project.settings 또는 DJANGO_SETTINGS_MODULE 환경변수를 설정해야 함

  • python path 관리
    pytest-django는 기본적으로 프로젝트의 manage.py 파일을 찾아보고 그 디렉토리를 python path에 자동으로 추가함

실행

$ pytest test.py directory

pytest-django는 manage.py 또는 django-admin.py를 이용해 테스트를 실행하지 않고, 단독으로 pytest 명령어를 실행하는 방식을 사용
위와 같이 실행 파라미터를 이용해 테스트를 실행할 대상 모듈과 디렉토리를 직접 지정할 수 있음

실행 옵션

pytest.ini에서 지정 가능

[pytest]
addopts = --reuse-db

—db-reuse

테스트 시작 시 DB를 만들고 삭제하는 작업을 하지 않음
소규모 테스트를 한다면 중요

—create-db

테스트 시작시 DB를 새로 만듬
models 변경 사항을 반영 할 수 있음
공식 문서에 따르면 default로 —db-reuse를 기본으로 설정해두고 DB 스키마 변경시에만 사용하는 것을 추천

—migration
models 스키마 변경이 있을 경우 migration을 진행
django DB 가 아닌 테스트용 DB에 적용

—no-migration
스키마 변경을 무시

데이터베이스 활용

  • pytest-django는 테스트할 때 DB를 접근하는 것에 대해 보수적으로 다룸
    따라서 기본적으로 테스트 과정에서 DB에 접근하려고 한다면 실패하게 됨

  • 테스트하려는 대상에서 DB에 접근을 하려면, 반드시 정확하게 명시해야만 허용
    DB가 필요한 테스트를 최소화하는 것도 좋은 선택

  • 테스트에서 DB 접근이 필요한 경우 pytset-django는 pytest mark를 사용

Markers

  • pytest.marks.db_django
    - DB 연결이 필요한 경우 marks를 활용
import pytest

@pytest.mark.django_db
def test_my_user():
	me = User.objects.get(username='me')
	assert me.is_superuser

클래스와 모듈 단위로 mark를 설정할 경우 모든 테스트에 적용할 수 있음

import pytest

pytestmark = pytest.mark.django_db

@pytest.mark.django_db
class TestUsers:
    pytestmark = pytest.mark.django_db
    def test_my_user(self):
        me = User.objects.get(username='me')
        assert me.is_superuser

Transaction

  • django 자체에 TransactionTestCase 클래스는 트랜젝션을 통해 격리된 상태에서 테스트를 수행하게 하고, 테스트를 마치면 DB 초기화를 해줌
    - 하지만 이 상태에서 수행되는 테스트는 트랜젝션 중에 생성된 DB 데이터를 비우는 과정 때문에 매우 느리게 수행될 수 있음
    - 이와 같은 기능을 사용하려면 django_db mark에 Transaction=True 파라미터를 전달
@pytest.mark.django_db(transaction=True)
def test_spam():
    pass

테스트 전용 데이터베이스

  • --reuse-db은 데이터베이스를 재사용하기 위한 실행 옵션이고, --create-db는 데이터베이스를 강제로 다시 생성하는 실행 옵션

  • 처음 테스트를 실행할 때 --reuse-db를 사용하면 새로운 테스트 전용 DB가 생성되는데 모든 테스트가 종료 되더라도 테스트 DB는 지워지지 않음
    - 그리고 다음 테스트를 실행할 때 동일하게 --reuse-db를 사용하면 이전 테스트 DB를 다시 사용하게 됨
    - 이 옵션은 적은 테스트를 실행할 때나 DB 테이블이 많은 경우 유용

  • --reuse-db 옵션을 기본 pytest.ini 옵션으로 지정하고, 스키마가 변경되었거나 했을 때 --create-db 옵션을 사용하는 것을 추천

[pytest]
addopts = --reuse-db
  • --nomigrations를 사용할 경우 django migrations와 모든 모델 클래스 검사를 위한 DB 생성을 수행하지 않음

Django Helper

Marker

  • pytest.marker를 이용해 테스트 함수나 클래스에 메타 데이터를 쉽게 설정할 수 있음

  • pytest.mark.django_db
    - 테스트 함수에서 DB 사용이 필요하다는 것을 나타냄
    - 모든 테스트는 각각의 DB 트랜젝션 안에서 수행되기 때문에 테스트가 종료되면 변경된 데이터도 함께 롤백

  • pytest.mark.urls
    - django의 URLCONF을 직접 지정할 수 있음
    - 예시: myapp.test_urls

Fixture

  • rf : django.utils.RequestFactory 인스턴스
    - middleware를 거치지 않고 바로 view로 연결되는 request를 만듬
    - 이럴 경우 request.user가 없어서 별도로 부착을 해줘야 함
from myapp.views import my_view

def test_details(rf, admin):
    request = rf.get('/customer/details')
    # Remember that when using RequestFactory, the request does not pass
    # through middleware. If your view expects fields such as request.user
    # to be set, you need to set them explicitly.
    # The following line sets request.user to an admin user.
    request.user = admin
    response = my_view(request)
    assert response.status_code == 200
  • client : django.test.Client 인스턴스
def test_with_client(client):
    response = client.get('/')
    assert response.content == 'Foobar'
profile
개발자 지망생

0개의 댓글