Django Web Framework/Django Pytest

[very academy] Pytest Mastery with Django

bluebamus 2023. 4. 2.
import pytest

# 테스트를 패스함
@pytest.mark.skip
def test_example():
    print("mark skip!!!")
    assert 1 == 1


# 현재는 실패하는 것이 정상으로 앞으로 개발, 개선될 코드임
# 결과 : ===== * passed, 1 xpassed =====
@pytest.mark.xfail
def test_example1():
    print("mark xfail!!!")
    assert 1 == 1


# slow라 마크된 것만 실행함
# pytest -m "slow"
@pytest.mark.slow
def test_example2():
    print("mark slow!!!")
    assert 1 == 1

# 에러/오류 발생
def test_example3():
    assert 1 == 2

유튜브 영상

https://www.youtube.com/playlist?list=PLOLrQ9Pn6caw3ilqDR8_qezp76QuEOlHY 

 

Pytest Mastery with Django

All code for this course can be found here: https://github.com/veryacademy/pytest-mastery-with-django Learn how to implement Pytest with Django. Welcome to t...

www.youtube.com

- pytest와 관련한 모든 학습 소스는 하나의 github 저장소에 관리 합니다.

https://github.com/bluebamus/django_pytest

 

GitHub - bluebamus/django_pytest: pytest projects

pytest projects. Contribute to bluebamus/django_pytest development by creating an account on GitHub.

github.com

- 해당 저장소의 "django_very_academy_setup_and_start_testing"를 확인하면 됩니다.

* 유튜브 영상의 코드는 버전상 문제가 있으며, 제가 관리하는 저장소 코드는 이를 해결한 코드입니다.

 

pytest.ini

[pytest]
DJANGO_SETTINGS_MODULE=config.settings.dev
addopts = -vv -s -W ignore::DeprecationWarning --reuse-db
python_files = tests.py test_*.py *_tests.py
filterwarnings = ignore::pytest.PytestConfigWarning

 - 현재 imp와 관련한 워닝이 확인되고 있으며, 이는 오랫동안 nose 패키지의 관리가 되지 않고 있는 문제로 imp는 현재 사용 패기가 된 패키지이다. 워닝을 무시하는 옵션을 넣으면 보기 편하다.

 

part1-1 - besic test

import pytest

# 테스트를 패스함
@pytest.mark.skip
def test_example():
    print("mark skip!!!")
    assert 1 == 1


# 현재는 개발/개선 사항이 있어 오류가 발생하는게 맞는 테스트
# 결과에 xfail이라는 항목이 추가되어 뜸
@pytest.mark.xfail
def test_example1():
    print("mark xfail!!!")
    assert 1 == 1

# slow라 마크된 항목만 실행
# pytest -m "slow"
@pytest.mark.slow
def test_example2():
    print("mark slow!!!")
    assert 1 == 1

# 오류 발생
def test_example3():
    assert 1 == 2

 

part1-2 - fixture and fixture factory

- @pytest.fixture()의 범위

   - function : 함수 단위로 fixture가 생성되고 삭제됨

   - class : 클래스 단위로 fixture가 생성되고 삭제됨

   - module : 하나의 모듈(파일) 단위로 생성되고 삭제됨

   - sessions : 한번의 테스트 단위로 생성되고 삭제됨

- 옵션

   -  autouse

   -  scope: 이 매개변수는 조명기의 범위를 지정합니다. 가능한 값은 "function", "class", "module", "session"입니다.

      기본적 으로 범위는 "함수"입니다.
   -  params: 이 매개변수는 조명기 함수에 값 목록을 전달하는 데 사용됩니다. 

      fixture 함수는 목록의 각 값에 대해 한 번씩 호출됩니다.
   -  autouse: 이 매개변수는 조명기를 종속성으로 선언하는 모든 테스트 함수에서 자동으로 사용해야 하는지 

      여부를 나타내는 부울 플래그입니다. autouse가 True이면 테스트 함수에 대한 인수로 지정할 필요 없이

      자동으로 픽스처가 사용됩니다. (모든 테스트에 인수로 정의가 되지 않아도 자동으로 실행됨)

import pytest
import time

@pytest.fixture(autouse=True)
def time_test(request):
    start_time = time.monotonic()
    yield
    end_time = time.monotonic()
    print(f"{request.node.name} took {end_time - start_time:.2f} seconds")

   -  name: 이 매개변수를 사용하면 테스트 기능에서 조명기를 참조하는 데 사용할 수 있는 조명기의 

      사용자 정의 이름을 지정할 수 있습니다.

      - name : num, expected_output

      - parameter : [(1, "one"), (2, "two"), (3, "three")]

      - 결과에 표시될 테스트별 id : ids=["test_one", "test_two", "test_three"]

import pytest

@pytest.mark.parametrize("num, expected_output", [(1, "one"), (2, "two"), (3, "three")], ids=["test_one", "test_two", "test_three"])
def test_convert_num_to_word(num, expected_output):
    assert convert_num_to_word(num) == expected_output
test_convert_num_to_word[test_one] PASSED
test_convert_num_to_word[test_two] PASSED
test_convert_num_to_word[test_three] FAILED

      - 만약 ids 선언이 없으면 아래와 같이 출력됨

test_convert_num_to_word[1-one]
test_convert_num_to_word[2-two]
test_convert_num_to_word[3-three]

part1-3 - DB fixture

- db fixture의 사용

- 아래 코드에서 scope에 따라 테스트 결과는 달라진다.

- 각 함수 범위에서만 동작하게 scope를 정의하면 테스트는 모두 통과된다.

- scope를 session으로 정의하면 에러가 발생한다. 이는 pytest의 db 연결에 엄격함 때문이다.

import pytest
from django.contrib.auth.models import User


@pytest.fixture(scope="session")  # error
def user_1(db):
    user = User.objects.create_user("test-user")
    print("create-user")
    return user


def test_set_check_password1(user_1):
    print("check-user1")
    assert user_1.username == "test-user"


def test_set_check_password2(user_1):
    print("check-user2")
    assert user_1.username == "test-user"

아래 코드는 동작한다.

import pytest
from django.contrib.auth.models import User

@pytest.fixture()
def user_1(db):
    return User.objects.create_user("test-user")


@pytest.mark.django_db
def test_set_check_password_true(user_1):
    user_1.set_password("new-password")
    assert user_1.check_password("new-password") is True


@pytest.mark.django_db
def test_set_check_password_fault(user_1):
    user_1.set_password("new-password")
    assert user_1.check_password("new-passwordd") is True

part1-4 - conftest.py

- 모든 테스트에 사용이 가능한 fixture를 만든다.

import pytest
from django.contrib.auth.models import User


@pytest.fixture()
def user_1(db):
    user = User.objects.create_user("test-user")
    print("create-user")
    return user


@pytest.fixture
def new_user_factory(db):
    def create_app_user(
        username: str,
        password: str = None,
        first_name: str = "firstname",
        last_name: str = "lastname",
        email: str = "test@test.com",
        is_staff: str = False,
        is_superuser: str = False,
        is_active: str = True,
    ):
        user = User.objects.create_user(
            username=username,
            password=password,
            first_name=first_name,
            last_name=last_name,
            email=email,
            is_staff=is_staff,
            is_superuser=is_superuser,
            is_active=is_active,
        )
        return user

    return create_app_user


@pytest.fixture
def new_user1(db, new_user_factory):
    return new_user_factory("Test_user", "password", "MyName")


@pytest.fixture
def new_user2(db, new_user_factory):
    return new_user_factory("Test_user", "password", "MyName", is_staff="True")

아래 코드는 conftest.py를 import 하지 않았음에도 파라미터로 정의, 사용이 가능하다.

import pytest
from django.contrib.auth.models import User


def test_new_user(new_user1):
    print(new_user1.first_name)
    assert new_user1.first_name == "MyName"


def test_new_user1(new_user2):
    print(new_user2.is_staff)
    assert new_user2.is_staff

 

part2-1 - factoryboy

- factoryboy에는 자체 fake가 있다. 

- 1000명의 임의의 사용자를 자동 생성하는 코드를 먼저 예시로 확인하자.

- factory.Faker()안이 값은 필드 이름이다.

- 다른 DB와의 트랜젝션 등의 복잡한 조건의 validate가 아닌경우, 조건에 부합되는 데이터를 생성해 준다.

import factory
from myapp.models import User

class UserFactory(factory.Factory):
    class Meta:
        model = User

    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    email = factory.Faker('email')
    password = factory.Faker('password')

# Generate 1000 new users using the factory
for i in range(1000):
    user = UserFactory.create()
    # Save the user object to your database or file

 

아래 코드에서 부터는

"3. Pytest   Django   Introducing Factory Boy and Faker - Fixture Replacement" 의 주요 코드를 설명한다.

 

1. factories.py

# app1/tests/factories.py

import factory
from faker import Faker
fake = Faker()

from django.contrib.auth.models import User 
from app1 import models


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    username = fake.name() # name 자동 생성
    is_staff = 'True'


class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Category

    name = 'django'


class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Product

    title = 'product_title'
    category = factory.SubFactory(CategoryFactory)  # 생성된 카테고리 연결
    description = fake.text()
    slug = 'product_slug'
    regular_price = '9.99'
    discount_price = '4.99'

2. conftest.py

- build() 메서드는 모델의 새 인스턴스를 생성하지만 데이터베이스에 저장하지는 않습니다. 테스트나 다른 목적으로 개체를 만들어야 하지만 데이터베이스에 유지하고 싶지 않은 경우에 유용합니다.

- create() 메서드는 모델의 새 인스턴스를 생성하고 데이터베이스에 저장합니다. 통합 테스트 또는 기능 테스트를 위한 테스트 데이터를 생성할 때와 같이 데이터베이스에 지속되는 객체를 생성해야 할 때 유용합니다.

   - 이 경우 test.py에 사용되는 테스트들에 pytest.mark.django.db를 사용해야 에러가 발생하지 않음

#conftest.py

import pytest

from pytest_factoryboy import register
from tests.factories import UserFactory, ProductFactory, CategoryFactory

# factory 데이터 등록
register(UserFactory)
register(ProductFactory)  
register(CategoryFactory)  

@pytest.fixture
def new_user1(db, user_factory):
    #user = user_factory.build() 
    user = user_factory.create()
    return user

3. test.py

# app1/tests/test_ex1.py

import pytest
from django.contrib.auth.models import User


# def test_new_user(new_user1):
#     print(new_user1.first_name)
#     assert new_user1.first_name == "MyName"


# def test_new_user1(new_user2):
#     print(new_user2.is_staff)
#     assert new_user2.is_staff


def test_new_user(user_factory):
    print(user_factory.username)
    assert True


@pytest.mark.django_db
def test_new_user1(user_factory):
    user=user_factory.create() # db에 데이터를 저장함
    count = User.objects.all().count()
    print('create - count : ',count)
    print('create - user.username : ',user.username)
    assert True


@pytest.mark.django_db
def test_new_user2(user_factory):
    user=user_factory.build() # db에 데이터를 저장하지 않음
    count = User.objects.all().count()
    print('build - count : ',count)
    print('build - user.username : ',user.username)
    assert True


def test_new_user3(new_user1):
    print('new_user1 - new_user1.username : ',new_user1.username)
    assert True


def test_product4(db, product_factory):
    product = product_factory.create() #category key가 없는 경우 build는 동작하지만 create는 에러가 발생함 
    print("product.description : " ,product.description)
    assert True

* factoryboy를 사용하는데 있어 네이밍 규칙 (chatGPT)

- pytest_factoryboy를 사용하여 Django의 pytest에서 팩토리를 정의할 때 따라야 하는 엄격한 명명 규칙은 없습니다.

  그러나 코드를 더 읽기 쉽고 유지 관리하기 쉽게 만들 수 있는 몇 가지 규칙이 있습니다.

   1. 팩토리 명명 규칙: 팩토리 클래스의 이름을 만드는 것을 명확하게 나타내는 방식으로 이름을 지정하는 것이 좋습니다.

       예를 들어 User라는 모델이 있는 경우 팩토리 클래스의 이름을 UserFactory로 지정할 수 있습니다. 마찬가지로

       Product라는 모델이 있는 경우 팩토리 클래스의 이름을 ProductFactory로 지정할 수 있습니다. 이 명명 규칙을 통해 각

       팩터리에서 생성하는 항목을 명확하게 알 수 있습니다.
   2 .팩토리 등록하기: pytest_factoryboy의 register() 함수를 사용하여 conftest.py 파일에 팩토리를 등록할 수 있습니다.

       팩토리를 등록할 때 유효한 Python 식별자인 한 팩토리의 이름을 선택할 수 있습니다. 예를 들어 UserFactory가 있는

      경우 register(UserFactory, "my_user_factory")와 같이 등록할 수 있습니다. 여기서 "my_user_factory"는 공장에

      제공하기 위해 선택한 이름입니다.
   3. 테스트 함수에서 팩토리 사용: 팩토리를 등록하면 픽스처로 포함하여 테스트 함수에서 사용할 수 있습니다. 조명기

       이름은 등록할 때 공장에 지정한 이름과 일치해야 합니다. 예를 들어 UserFactory를 "my_user_factory"라는

       이름으로 등록한 경우 다음과 같은 테스트 함수에서 사용할 수 있습니다.

def test_my_user(my_user_factory):
    user = my_user_factory.create()
    assert user.username == "johndoe"

   4. register(UserFactory)의 경우 결과 팩토리 함수는 기본적으로 user_factory로 이름이 지정됩니다. 팩토리 이름은

      모델명에서 첫 글자를 소문자로 변환하고 _factory를 붙여서 파생하기 때문입니다. 따라서 UserFactory는

      user_factory가 됩니다.

   5. 다른 모델 팩터리의 경우 결과 팩터리 함수는 동일한 명명 규칙을 따릅니다. 예를 들어 ProductFactory는

       product_factory가 되고 CategoryFactory는 category_factory가 됩니다.

   6. 테스트 함수에서 팩토리에 대한 매개변수 이름은 엄격하게 정의되어 있지 않으나 결과 팩토리 함수와 동일한 이름에

       밑줄로 구분된 단어를 사용하는 것이 좋습니다. 예를 들어 UserFactory를 등록한 경우 user_factory를 테스트 함수의

       매개변수 이름으로 사용할 수 있습니다.

part3-1 towards parametrizing fixtures and test functions

factoryies.py

import factory
from faker import Faker

fake = Faker()

from app1 import models
from django.contrib.auth.models import User


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    username = fake.name()
    is_staff = 'True'


class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Category

    name = 'django'


class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Product

    title = 'product_title'
    category = factory.SubFactory(CategoryFactory)
    description = fake.text()
    slug = 'product_slug'
    regular_price = '9.99'
    discount_price = '4.99'

conftest.py

import pytest

from pytest_factoryboy import register
from tests.factories import UserFactory, ProductFactory, CategoryFactory

register(UserFactory)
register(ProductFactory)  
register(CategoryFactory)  

@pytest.fixture
def new_user1(db, user_factory):
    user = user_factory.create()
    return user

test_ex5.py

import pytest
from app1.models import Product


@pytest.mark.parametrize(
    "title, category, description, slug, regular_price, discount_price, validity",
    [
        ("NewTitle", 1, "NewDescription", "slug", "4.99", "3.99", True),
        ("NewTitle", 1, "NewDescription", "slug", "", "3.99", False),
    ],
)
def test_product_instance(
    db,
    product_factory,
    title,
    category,
    description,
    slug,
    regular_price,
    discount_price,
    validity,
):
    test = product_factory(
        title=title,
        category_id=category,
        description=description,
        slug=slug,
        regular_price=regular_price,
        discount_price=discount_price,
    )

    item = Product.objects.all().count()
    print(item)
    assert item == validity

- 주요 코드 설명

@pytest.mark.parametrize(
    # 함수의 파라미터로 사용되는 name 항목
    "title, category, description, slug, regular_price, discount_price, validity",
    # 함수의 파라미터에 적용될 값 항목
    [
        ("NewTitle", 1, "NewDescription", "slug", "4.99", "3.99", True),
        ("NewTitle", 1, "NewDescription", "slug", "", "3.99", False),
    ],
)

# 아래 함수에서 매칭되는 파라미터 name에 리스트 값이 적용됨
def test_product_instance(
    db,
    product_factory,
    title,
    category,
    description,
    slug,
    regular_price,
    discount_price,
    validity,
):

part4-1 - intro testing with pytest selenium and django

- Django.test의 LiveServerTestCase 설명 (chatGPT)

   - TestCase 클래스의 하위 클래스이며 실행 중인 웹 서버가 필요한 Django 웹 애플리케이션을 테스트하는 데 사용됩니

     다. 이 클래스는 각 테스트를 실행하기 전에 서버의 새 인스턴스를 자동으로 생성하고 테스트가 완료된 후 이를 삭제합

     니다.
   - LiveServerTestCase는 클라이언트 측 JavaScript를 테스트하거나 이메일 서비스, 지불 게이트웨이 또는 소셜 미디어 플

     랫폼과 같은 외부 서비스와의 상호 작용이 필요한 기능을 테스트할 때 특히 유용합니다. 이를 통해 최종 사용자가 경험

    하는 것과 유사한 실제 환경에서 이러한 기능을 테스트할 수 있습니다.
   - 사용 예시 : 

from django.test import LiveServerTestCase
from selenium import webdriver

class MyTestCase(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = webdriver.Firefox()
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_my_feature(self):
        self.selenium.get(self.live_server_url + '/my-url/')
        # perform some actions on the page using Selenium
        # assert the expected results

conftest.py

import pytest
from selenium import webdriver

import pytest

# from pytest_factoryboy import register
# from tests.factories import UserFactory, ProductFactory, CategoryFactory

# register(UserFactory)
# register(ProductFactory)  
# register(CategoryFactory)  

# @pytest.fixture
# def new_user1(db, user_factory):
#     #user = user_factory.build()
#     user = user_factory.create()
#     return user

# from pytest_factoryboy import register
# from tests.factories import UserFactory, ProductFactory, CategoryFactory

# register(UserFactory)
# register(ProductFactory)
# register(CategoryFactory)

# @pytest.fixture
# def new_user1(db, user_factory):
#     user = user_factory.create()
#     return user


# @pytest.fixture(scope="class")
# def chrome_driver_init(request):

#     options = webdriver.ChromeOptions()
#     options.add_argument("--headless")
#     chrome_driver = webdriver.Chrome(executable_path=r"./chromedriver", options=options)
#     request.cls.driver = chrome_driver
#     yield
#     chrome_driver.close()


@pytest.fixture(params=["chrome", "firefox"], scope="class")
def driver_init(request):
    if request.param == "chrome":
        options = webdriver.ChromeOptions()
        options.add_argument("--headless")
        web_driver = webdriver.Chrome(executable_path=r"./chromedriver", options=options)
    if request.param == "firefox":
        options = webdriver.FirefoxOptions()
        options.add_argument("--headless")
        web_driver = webdriver.Firefox(executable_path=r"./geckodriver", options=options)
    request.cls.driver = web_driver
    yield
    web_driver.close()

test_ex1.py

import pytest
from django.test import LiveServerTestCase
from selenium import webdriver

# Example 1
# class TestBrowser1(LiveServerTestCase):
#     def test_example(self):
#         driver = webdriver.Chrome("./chromedriver")
#         driver.get(("%s%s" % (self.live_server_url, "/admin/")))
#         assert "Log in | Django site admin" in driver.title


# Example 2
# class TestBrowser2(LiveServerTestCase):
#     def test_example(self):
#         options = webdriver.ChromeOptions()
#         options.add_argument("--headless")
#         driver = webdriver.Chrome(executable_path=r"./chromedriver", options=options)
#         driver.get(("%s%s" % (self.live_server_url, "/admin/")))
#         assert "Log in | Django site admin" in driver.title

# Example 3
# Fixture for Chrome
# @pytest.fixture(scope="class")
# def chrome_driver_init(request):

#     options = webdriver.ChromeOptions()
#     options.add_argument("--headless")
#     chrome_driver = webdriver.Chrome(executable_path=r"./chromedriver", options=options)
#     request.cls.driver = chrome_driver
#     yield
#     chrome_driver.close()


# @pytest.mark.usefixtures("chrome_driver_init")
# class Test_URL_Chrome(LiveServerTestCase):
#     def test_open_url(self):
#         self.driver.get(("%s%s" % (self.live_server_url, "/admin/")))
#         assert "Log in | Django site admin" in self.driver.title


# @pytest.fixture(params=["chrome", "firefox"], scope="class")
# def driver_init(request):
#     if request.param == "chrome":
#         options = webdriver.ChromeOptions()
#         options.add_argument("--headless")
#         web_driver = webdriver.Chrome(executable_path=r"./chromedriver", options=options)
#     if request.param == "firefox":
#         options = webdriver.FirefoxOptions()
#         options.add_argument("--headless")
#         web_driver = webdriver.Firefox(executable_path=r"./geckodriver", options=options)
#     request.cls.driver = web_driver
#     yield
#     web_driver.close()


@pytest.mark.usefixtures("driver_init")
class Test_URL_Chrome:
    def test_open_url(self, live_server):
        self.driver.get(("%s%s" % (live_server.url, "/admin/")))
        assert "Log in | Django site admin" in self.driver.title

- 저장소의 part5 소스를 실행하면 아래와 같은 에러가 발생한다.

conftest.py:2: in <module>
    from selenium import webdriver
E   ModuleNotFoundError: No module named 'selenium'

- 해결 방법으로 설치한 selenium과 호환되는 python 버전으로 설치 및 변경을 해주면 된다.

- chromedriver.exe, geckodriver.exe 파일들과의 호환성도 확인을 해야한다.

- 만약 실행이 되지 않는다면 다음 순서로 실행해 본다.

   1. 기본이 되는 selenium을 설치한다.

   2.  selenium의 버전을 확인하고 호환성 문제가 없는 실행 파일과 python 버전을 맞춘다.

part5-1 - taking screenshots

- django test에서 사용되는 LiveServerTestCase와 live_server의 정리 (chatGPT)

1. 두 라이브러리의 정의

   - LiveServerTestCase는 웹 애플리케이션을 테스트하기 위해 Django의 테스트 프레임워크에서 제공하는 클래스입니다. pytest의 live_server 고정 장치와 유사하지만 몇 가지 주요 차이점이 있습니다.

   - LiveServerTestCase와 live_server의 주요 차이점은 LiveServerTestCase는 클래스 기반 테스트인 반면 live_server는 함수 기반 테스트에 사용되는 픽스처라는 것입니다. 즉, LiveServerTestCase에서는 테스트를 클래스의 메서드로 정의하고 live_server에서는 테스트를 개별 함수로 정의합니다.

   - 또 다른 주요 차이점은 LiveServerTestCase가 서버 구성에 대한 더 많은 제어를 제공한다는 것입니다. LiveServerTestCase를 사용하면 서버의 주소와 포트를 지정할 수 있으며 LiveServerTestCase 클래스를 서브클래싱하고 사용자 지정 메서드를 추가하여 서버의 동작을 사용자 지정할 수도 있습니다.

 

2. 가상 시나리오 기반 사용 예시

   - LiveServerTestCase

      - 시나리오 1: 복잡한 서버 구성이 필요한 Django 웹 애플리케이션 테스트
      - 다중 서버, 부하 분산 또는 사용자 지정 미들웨어와 같은 복잡한 서버 구성이 필요한 Django 웹 애플리케이션을 테스트한다고 가정합니다. 이 경우 서버 구성 및 동작을 사용자 지정할 수 있으므로 LiveServerTestCase가 더 나은 선택일 수 있습니다.
      - 예를 들어 사용자 정의 구성 옵션을 사용하여 여러 서버 인스턴스를 설정하는 사용자 정의 LiveServerTestCase 하위 클래스를 만들 수 있습니다.
      - 이 예제에서는 사용자 지정 미들웨어 및 구성 옵션을 사용하여 두 개의 서버 인스턴스를 설정하는 사용자 지정 MyCustomLiveServerTestCase 클래스를 정의합니다. setUpClass 및 tearDownClass 메서드를 사용하여 서버 인스턴스를 생성 및 해제하고 일부 테스트 작업을 수행하기 위해 사용자 지정 서버 인스턴스를 사용하는 test_something이라는 테스트 메서드를 정의합니다.

from django.test import LiveServerTestCase
from myapp.middleware import MyCustomMiddleware

class MyCustomLiveServerTestCase(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.server1 = cls._create_server_instance("127.0.0.1", 8000, middleware=[MyCustomMiddleware])
        cls.server2 = cls._create_server_instance("127.0.0.1", 8001)

    @classmethod
    def tearDownClass(cls):
        cls.server1.shutdown()
        cls.server2.shutdown()
        super().tearDownClass()

    def test_something(self):
        # test actions using the custom server instances

    @staticmethod
    def _create_server_instance(address, port, middleware=None):
        # create and configure a custom server instance

   - live_server

      - 시나리오 2: 간단한 서버 구성이 필요한 Flask API 테스트
      - 기본 옵션이 있는 단일 서버 인스턴스와 같은 간단한 서버 구성이 필요한 Flask API를 테스트한다고 가정합니다. 이 경우 pytest의 live_server 픽스처가 더 간단하고 가벼우므로 더 나은 선택일 수 있습니다.
      - 예를 들어 Flask API를 테스트하기 위해 live_server 픽스처를 사용하는 pytest 테스트 함수를 정의할 수 있습니다.
      - 이 예제에서는 라이브 서버에 GET 요청을 보내고 응답 상태 코드를 테스트하기 위해 live_server 픽스처를 사용하는 test_api라는 테스트 함수를 정의합니다. live_server 고정 장치는 서버 인스턴스 설정 및 해제를 처리하므로 서버 구성 또는 관리에 대해 걱정할 필요가 없습니다.

import pytest
import requests

@pytest.fixture(scope="session")
def live_server(request, live_server):
    # Perform setup actions, if any
    yield live_server
    # Perform teardown actions, if any

def test_api(live_server):
    response = requests.get(live_server.url + "/api")
    assert response.status_code == 200

- conftest.py와 test.py에 동일한 이름, scope의 fixture가 있다면 어떤 fixture가 실행되는가?

   - 이 경우, 동일한 test.py 안의 fixture가 우선권이 높다. 같은 파일내 fixture의 이름을 찾을 수 없을 경우, conftest.py의 내용을 확인한다. 

 

- 동일한 test.py에 동일한 이름, function, module, session의 scope의 fixture가 있다면?

   - 테스트 함수가 픽스처를 호출할 때 pytest가 function, class, module 및 session의 순서로 같은 이름의 fixture를 검색하며 이 경우, function 범위가 가장 높은 우선 순위를 가지므로 먼저 선택됩니다.

 

- test.py

import os
import time

import pytest
from selenium import webdriver


def take_screenshot(driver, name):
    time.sleep(1)
    os.makedirs(os.path.join("screenshot", os.path.dirname(name)), exist_ok=True)
    driver.save_screenshot(os.path.join("screenshot", name))


# def test_example(live_server):
#     options = webdriver.ChromeOptions()
#     options.add_argument("--headless")
#     options.add_argument("--window-size=1920,1080")
#     chrome_driver = webdriver.Chrome("./chromedriver", options=options)
#     chrome_driver.get(("%s%s" % (live_server.url, "/admin/")))
#     take_screenshot(chrome_driver, "admin/admin.png")


@pytest.fixture(params=["chrome1920", "chrome411", "firefox"], scope="class")
def driver_init(request):
    if request.param == "chrome1920":
        options = webdriver.ChromeOptions()
        options.add_argument("--headless")
        options.add_argument("--window-size=1920,1080")
        web_driver = webdriver.Chrome(
            executable_path=r"./chromedriver", options=options
        )
        request.cls.browser = "Chrome1920x1080"
    if request.param == "chrome411":
        options = webdriver.ChromeOptions()
        options.add_argument("--headless")
        options.add_argument("--window-size=411,823")
        web_driver = webdriver.Chrome(
            executable_path=r"./chromedriver", options=options
        )
        request.cls.browser = "Chrome411x823"
    if request.param == "firefox":
        options = webdriver.FirefoxOptions()
        options.add_argument("--headless")
        options.binary_location = r"C:/Program Files/Mozilla Firefox/firefox.exe"
        web_driver = webdriver.Firefox(
            executable_path=r"./geckodriver", options=options
        )
        request.cls.browser = "Firefox"
    request.cls.driver = web_driver
    yield
    web_driver.close()


@pytest.mark.usefixtures("driver_init")
class Screenshot:
    def screenshot_admin(self, live_server):
        self.driver.get(("%s%s" % (live_server.url, "/admin/")))
        take_screenshot(self.driver, "admin/" + "admin" + self.browser + ".png")
        assert "Log in | Django site admin" in self.driver.title

- 위 코드를 실행할 때, 파이어 폭스 브라우저가 설치 되어 있어야 동작한다.

- 이전 테스트에서는 "options.binary_location" 옵션 없이 동작을 했는데, 윈도우11 환경에서 테스트를 해보니 에러가 발생했다.

- 에러 내용 :

Screenshot::screenshot_admin[firefox] - selenium.common.exceptions.SessionNotCreatedException: Message: Expected browser binary location, but unable to find binary in default location, no 'moz:firefoxOptions.binary' capability provided, and no binary flag set on the command line

- 해결 방안으로 options.binary_location = r"C:/Program Files/Mozilla Firefox/firefox.exe"을 설정해준다.

 

- 기본적으로 class 혹은 functions은 이름 앞에 test_가 붙어야 한다. 하지만 패턴을 바꿀 수도 있다.

   - pytest.ini 에 다음과 같이 정의 하면 된다.

[pytest]
DJANGO_SETTINGS_MODULE = core.settings
python_files = test_*.py
python_classes = Screenshot Test_*
python_functions = test_* screenshot_*

markers =
    slow: slow running test

 

 

* 리눅스 서버에서는 gui와 관련한 설정과 header에 대한 설정을 별개로 해줘야 하는 경우가 있다.

   - 리눅스 서버에서는 캡쳐된 결과가 실재 디자인과 다를 수 있다. 이는 실재 웹 브라우저의 렌더링 없이 동작하기 때문에 발생할 수 있다.

   - vuejs, reactjs 등의 자바스크립트 기반에서는 동적 렌더링 문제로 캡쳐가 문제가 생길 수 있다. 별도 제어가 필요하다.

 

 

reference

https://jakpentest.tistory.com/275

 

pytest에서 자주 사용했던 것들

개요 pytest는 unittest와 더불어 python에서 테스트 코드를 작성하기 위해 많이 사용되는 라이브러리입니다. unittest와는 조금 다르게 테스트 코드를 작성하기에 간결하고 여러 기능을 지원하기 때문

jakpentest.tistory.com

https://velog.io/@hwaya2828/Django-Pytest

 

[Django] Pytest

Pytest란?

velog.io

https://pytest-factoryboy.readthedocs.io/en/stable/

 

Welcome to pytest-factoryboy’s documentation! — pytest-factoryboy 2.5.1 documentation

Post-generation dependencies Unlike factory_boy which binds related objects using an internal container to store results of lazy evaluations, pytest-factoryboy relies on the PyTest request. Circular dependencies between objects can be resolved using post-g

pytest-factoryboy.readthedocs.io

 

댓글