From b59c8fbf8bc635b711d925dde15a98cf397243dd Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Fri, 8 Sep 2023 16:17:35 +0300 Subject: [PATCH] added better test fixtures --- akarpov/conftest.py | 122 ++++++++++++++++++++++-- akarpov/users/tests/factories.py | 51 +++------- akarpov/users/tests/test_models.py | 10 +- akarpov/utils/faker.py | 4 +- akarpov/utils/pytest_factoryboy.py | 47 ++++++++++ poetry.lock | 143 ++++++++++++++++++++++++++++- pyproject.toml | 5 + pytest.ini | 5 +- 8 files changed, 333 insertions(+), 54 deletions(-) create mode 100644 akarpov/utils/pytest_factoryboy.py diff --git a/akarpov/conftest.py b/akarpov/conftest.py index 0850c00..edcfa66 100644 --- a/akarpov/conftest.py +++ b/akarpov/conftest.py @@ -1,14 +1,122 @@ -import pytest +import io +import logging +import re -from akarpov.users.models import User -from akarpov.users.tests.factories import UserFactory +import factory +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile +from pytest_django.lazy_django import skip_if_no_django +from rest_framework.test import APIClient + +from akarpov.utils.config import build_redis_uri +from akarpov.utils.faker import configure_factory_faker, configure_faker +from akarpov.utils.pytest_factoryboy import autodiscover_factories + +configure_factory_faker(factory.Faker) + +autodiscover_factories() + +logger = logging.getLogger(__name__) @pytest.fixture(autouse=True) -def media_storage(settings, tmpdir): - settings.MEDIA_ROOT = tmpdir.strpath +def activate_django_db(db): + pass + + +@pytest.fixture(scope="session", autouse=True) +def update_config(): + from django.conf import settings + + settings.DEBUG = True @pytest.fixture -def user(db) -> User: - return UserFactory() +def api_client(): + return APIClient() + + +@pytest.fixture +def admin_client(client, user_factory): + admin_user = user_factory(is_superuser=True, is_staff=True) + client.force_login(admin_user) + return client + + +@pytest.fixture +def api_user_client(api_client, user): + api_client.force_authenticate(user) + return api_client + + +@pytest.fixture +def image_factory(): + def factory(filename="test.jpg", **params): + from PIL import Image + + width = params.get("width", 520) + height = params.get("height", width) + color = params.get("color", "blue") + image_format = params.get("format", "JPEG") + image_palette = params.get("palette", "RGB") + + thumb_io = io.BytesIO() + with Image.new(image_palette, (width, height), color) as thumb: + thumb.save(thumb_io, format=image_format) + return SimpleUploadedFile(filename, thumb_io.getvalue()) + + return factory + + +@pytest.fixture +def image(image_factory): + return image_factory() + + +@pytest.fixture +def uploaded_photo(image): + return SimpleUploadedFile("test.png", image.read()) + + +@pytest.fixture +def plain_file(): + return io.BytesIO(b"plain_text") + + +@pytest.fixture(scope="session", autouse=True) +def faker_session_locale(): + return ["en"] + + +@pytest.fixture(autouse=True) +def add_faker_providers(faker): + configure_faker(faker) + return faker + + +@pytest.fixture(scope="session", autouse=True) +def set_test_redis_databases(request): + from django.conf import settings + + skip_if_no_django() + xdist_worker = getattr(request.config, "workerinput", {}).get("workerid") + if xdist_worker is None: + return + worker_number_search = re.search(r"\d+", xdist_worker) + if not worker_number_search: + return + max_db_number = worker_number_search[0] + max_db_number = int(max_db_number) * 3 + 2 + channels_redis_url = build_redis_uri( + settings.CHANNELS_REDIS_HOST, + settings.CHANNELS_REDIS_PORT, + settings.CHANNELS_REDIS_USER, + settings.CHANNELS_REDIS_PASSWORD, + max_db_number, + ) + settings.CHANNEL_LAYERS["default"]["CONFIG"]["hosts"][0][ + "address" + ] = channels_redis_url + settings.CHANNELS_REDIS_DB = max_db_number + settings.CLICKHOUSE_REDIS_CONFIG["db"] = max_db_number - 1 + settings.CELERY_REDIS_DB = max_db_number - 2 diff --git a/akarpov/users/tests/factories.py b/akarpov/users/tests/factories.py index 5246936..0ca44a9 100644 --- a/akarpov/users/tests/factories.py +++ b/akarpov/users/tests/factories.py @@ -1,45 +1,20 @@ -from collections.abc import Sequence -from typing import Any - -from django.contrib.auth import get_user_model -from factory import Faker, post_generation +import factory.fuzzy from factory.django import DjangoModelFactory -from akarpov.utils.faker import django_image +from akarpov.utils.pytest_factoryboy import global_register +@global_register class UserFactory(DjangoModelFactory): - username = Faker("user_name") - email = Faker("email") - name = Faker("name") - about = Faker("text") - - @post_generation - def password(self, create: bool, extracted: Sequence[Any], **kwargs): - password = ( - extracted - if extracted - else Faker( - "password", - length=42, - special_chars=True, - digits=True, - upper_case=True, - lower_case=True, - ).evaluate(None, None, extra={"locale": None}) - ) - self.set_password(password) - - @post_generation - def image(self, create, extracted, **kwargs): - if extracted: - image_name, image = extracted - else: - image_name = "test.jpg" - image = django_image(image_name, **kwargs) - self.image.save(image_name, image) + email = factory.Sequence(lambda i: f"user_{i}@akarpov.ru") + username = factory.Faker("word") + image = factory.fuzzy.FuzzyText(prefix="https://img") + password = "P@ssw0rd" class Meta: - model = get_user_model() - skip_postgeneration_save = False - django_get_or_create = ["username"] + model = "users.User" + + @classmethod + def _create(cls, model_class, *args, **kwargs): + manager = cls._get_manager(model_class) + return manager.create_user(*args, **kwargs) diff --git a/akarpov/users/tests/test_models.py b/akarpov/users/tests/test_models.py index a4816a2..dbb31ac 100644 --- a/akarpov/users/tests/test_models.py +++ b/akarpov/users/tests/test_models.py @@ -1,17 +1,19 @@ from akarpov.files.consts import USER_INITIAL_FILE_UPLOAD -from akarpov.users.models import User -def test_user_create(user: User): +def test_user_create(user_factory): + user = user_factory() password = "123" user.set_password(password) assert user.check_password(password) -def test_auto_file_upload_size(user: User): +def test_auto_file_upload_size(user_factory): + user = user_factory() size = USER_INITIAL_FILE_UPLOAD assert user.left_file_upload == size -def test_user_image_create(user: User): +def test_user_image_create(user_factory): + user = user_factory() assert user.image diff --git a/akarpov/utils/faker.py b/akarpov/utils/faker.py index 0399428..265cb30 100644 --- a/akarpov/utils/faker.py +++ b/akarpov/utils/faker.py @@ -15,9 +15,9 @@ def money(self): def configure_factory_faker(factory_faker): - factory_faker._DEFAULT_LOCALE = "ru_RU" + factory_faker._DEFAULT_LOCALE = "en" for provider in additional_providers: - factory_faker.add_provider(provider, locale="ru_RU") + factory_faker.add_provider(provider, locale="en") def configure_faker(faker): diff --git a/akarpov/utils/pytest_factoryboy.py b/akarpov/utils/pytest_factoryboy.py new file mode 100644 index 0000000..9dfaea8 --- /dev/null +++ b/akarpov/utils/pytest_factoryboy.py @@ -0,0 +1,47 @@ +from django.utils.module_loading import autodiscover_modules +from pytest_factoryboy.fixture import get_caller_locals, register + + +class RegisteredFactory: + def __init__(self, factory_class, args, kwargs): + self.factory_class = factory_class + self.args = args + self.kwargs = kwargs + + +factory_registry = set() # set of registered factories + + +def global_register(factory_class=None, *args, **kwargs): + if factory_class is None: + + def _global_register(factory_class): + return global_register(factory_class, *args, **kwargs) + + return _global_register + + factory_registry.add(RegisteredFactory(factory_class, args, kwargs)) + + return factory_class + + +def autodiscover_factories(): + assert ( + not factory_registry + ), "You've already called `autodiscover_factories` function" + + caller_locals = get_caller_locals() + + assert caller_locals["__name__"].endswith( + "conftest" + ), "You must call `autodiscover_factories` from `conftest.py` file" + + autodiscover_modules("tests.factories") + + for registered_factory in factory_registry: + register( + registered_factory.factory_class, + *registered_factory.args, + _caller_locals=caller_locals, + **registered_factory.kwargs, + ) diff --git a/poetry.lock b/poetry.lock index dd6fcbf..f2a1a9b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -213,6 +213,18 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.22)"] +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + [[package]] name = "appnope" version = "0.1.3" @@ -2213,6 +2225,21 @@ files = [ dnspython = ">=2.0.0" idna = ">=2.0.0" +[[package]] +name = "execnet" +version = "2.0.2" +description = "execnet: rapid multi-Python deployment" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "executing" version = "1.2.0" @@ -3589,6 +3616,26 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=0.29.35)"] +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown" version = "3.4.4" @@ -5202,6 +5249,25 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "pytest-django" version = "4.5.2" @@ -5221,6 +5287,60 @@ pytest = ">=5.4.0" docs = ["sphinx", "sphinx-rtd-theme"] testing = ["Django", "django-configurations (>=2.0)"] +[[package]] +name = "pytest-factoryboy" +version = "2.3.1" +description = "Factory Boy support for pytest." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-factoryboy-2.3.1.tar.gz", hash = "sha256:5102ce8597d1d8db2436f1fcfe96635f212801e18e14c2f04327a9343b9b56d7"}, + {file = "pytest_factoryboy-2.3.1-py3-none-any.whl", hash = "sha256:e8c249c8c5c195ecd46f86377dbe92451372295a9485d42060e3f82ee23b1ff6"}, +] + +[package.dependencies] +appdirs = "*" +factory-boy = ">=2.10.0" +inflection = "*" +mako = "*" +pytest = ">=5.0.0" +typing-extensions = "*" + +[[package]] +name = "pytest-lambda" +version = "2.2.0" +description = "Define pytest fixtures with lambda functions." +category = "main" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "pytest-lambda-2.2.0.tar.gz", hash = "sha256:f8af7b011980b04499d906161b69e7df984a8b16285a17ca97eb133a0775dd58"}, + {file = "pytest_lambda-2.2.0-py3-none-any.whl", hash = "sha256:ad77ff48a514379bfccb404ba9d3f5871c2a0817b47ccc8abbad430d6d450ef4"}, +] + +[package.dependencies] +pytest = ">=3.6,<8" +wrapt = ">=1.11.0,<2.0.0" + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "pytest-sugar" version = "0.9.7" @@ -5241,6 +5361,27 @@ termcolor = ">=2.1.0" [package.extras] dev = ["black", "flake8", "pre-commit"] +[[package]] +name = "pytest-xdist" +version = "3.3.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"}, + {file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-crontab" version = "3.0.0" @@ -8043,4 +8184,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "78a5ef3c902cae4b5e808da8eed517b8b649800ca567d67a7c004eb1b5de8147" +content-hash = "07c6933d3ce4f4d081cffbe7b7ad4e3561a0353e1f8157961916ca7502549914" diff --git a/pyproject.toml b/pyproject.toml index 93f0681..ca2f03d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,11 @@ requests = ">=2.25" spacy = {extras = ["lookups"], version = "^3.6.1"} spacy-transformers = "^1.2.5" extract-msg = "0.28.7" +pytest-factoryboy = "2.3.1" +pytest-xdist = "^3.3.1" +pytest-mock = "^3.11.1" +pytest-asyncio = "^0.21.1" +pytest-lambda = "^2.2.0" [build-system] diff --git a/pytest.ini b/pytest.ini index c2b3a23..a772fa0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] -addopts = --ds=config.settings.test --reuse-db -python_files = tests.py test_*.py +DJANGO_SETTINGS_MODULE = config.settings.test +python_files = tests.py test_*.py *_tests.py +addopts = --reuse-db