Skip to content

Commit

Permalink
Merge pull request #163 from pytest-dev/fixture_name_from_factory
Browse files Browse the repository at this point in the history
Determine the fixture name using the factory name, rather than the model
  • Loading branch information
youtux committed Jun 3, 2022
2 parents 3a2a2c9 + 1801f00 commit 56dab10
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Changelog

Unreleased
----------
- The fixture name for registered factories is now determined by the factory name (rather than the model name). This makes factories for builtin types (like ``dict``) easier to use.

2.4.0
----------
Expand Down
54 changes: 18 additions & 36 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ pytest-factoryboy makes it easy to combine ``factory`` approach to the test setu
heart of the `pytest fixtures`_.

.. _factory_boy: https://factoryboy.readthedocs.io
.. _pytest: http://pytest.org
.. _pytest: https://pytest.org
.. _pytest fixtures: https://pytest.org/latest/fixture.html
.. _overridden: http://pytest.org/latest/fixture.html#override-a-fixture-with-direct-test-parametrization
.. _overridden: https://docs.pytest.org/en/latest/how-to/fixtures.html#overriding-fixtures-on-various-levels


Install pytest-factoryboy
Expand All @@ -35,35 +35,11 @@ Concept
Library exports a function to register factories as fixtures. Fixtures are contributed
to the same module where register function is called.

Factory Fixture
---------------

Factory fixtures allow using factories without importing them. Name convention is lowercase-underscore
class name.

.. code-block:: python
import factory
from pytest_factoryboy import register
class AuthorFactory(factory.Factory):
class Meta:
model = Author
register(AuthorFactory)
def test_factory_fixture(author_factory):
author = author_factory(name="Charles Dickens")
assert author.name == "Charles Dickens"
Model Fixture
-------------

Model fixture implements an instance of a model created by the factory. Name convention is model's lowercase-underscore
class name.
Model fixture implements an instance of a model created by the factory. The fixture name is determined by the factory name.
For example, if the factory name is ``MyAuthorFactory``, the fixture name is ``my_author``.


.. code-block:: python
Expand All @@ -83,29 +59,35 @@ class name.
assert author.name == "Charles Dickens"
Model fixtures can be registered with specific names. For example, if you address instances of some collection
by the name like "first", "second" or of another parent as "other":
Model fixtures can be registered with specific names.
You may want to use this if your factory name does not follow the naming convention, or if you need multiple model fixtures for the same factory:


.. code-block:: python
register(AuthorFactory) # author
register(AuthorFactory, "second_author") # second_author
# `register(...)` can be used as a decorator too
@register # book
@register(_name="second_book") # second_book
@register(_name="other_book") # other_book, book of another author
class BookFactory(factory.Factory):
class Meta:
model = Book
@pytest.fixture
def other_book__author(second_author):
"""Make the relation of the second_book to another (second) author."""
return second_author
The ``register`` function can be used both as a decorator and as a function. It will register the fixtures in the module/class where it's invoked.

.. code-block:: python
# register used as a function:
register(AuthorFactory) # `author` is now a fixture of this module
register(AuthorFactory, "second_author") # `second_author` too
# register used as a decorator:
@register # `book` is now a fixture of this module.
class BookFactory(factory.Factory):
class Meta:
model = Book
Attributes are Fixtures
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ typing_extensions = "*"
[tool.poetry.dev-dependencies]
mypy = "^0.960"
tox = "^3.25.0"
packaging = "^21.3"
importlib-metadata = { version = "^4.11.4", python = "<3.8" }

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
17 changes: 12 additions & 5 deletions pytest_factoryboy/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,18 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[st

def get_model_name(factory_class: FactoryType) -> str:
"""Get model fixture name by factory."""
return (
inflection.underscore(factory_class._meta.model.__name__)
if not isinstance(factory_class._meta.model, str)
else factory_class._meta.model
)
# "class AuthorFactory(...):" -> "author"
suffix = "Factory"
if factory_class.__name__.endswith(suffix):
factory_name = factory_class.__name__[: -len(suffix)]
return inflection.underscore(factory_name)
else:
raise ValueError(
f"Factory {factory_class} does not follow the '<model_name>Factory' naming convention and "
"pytest-factoryboy cannot automatically determine the fixture name.\n"
f"Please use the naming convention '<model_name>Factory' or explicitly set the fixture name "
"using `@register(_name=...)`."
)


def get_factory_name(factory_class: FactoryType) -> str:
Expand Down
55 changes: 55 additions & 0 deletions tests/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

import sys

from _pytest.pytester import RunResult
from packaging.version import Version

if sys.version_info >= (3, 8):
from importlib import metadata
else:
import importlib_metadata as metadata

PYTEST_VERSION = Version(metadata.version("pytest"))

if PYTEST_VERSION >= Version("6.0.0"):

def assert_outcomes(
result: RunResult,
passed: int = 0,
skipped: int = 0,
failed: int = 0,
errors: int = 0,
xpassed: int = 0,
xfailed: int = 0,
) -> None:
"""Compatibility function for result.assert_outcomes"""
result.assert_outcomes(
errors=errors,
passed=passed,
skipped=skipped,
failed=failed,
xpassed=xpassed,
xfailed=xfailed,
)

else:

def assert_outcomes(
result: RunResult,
passed: int = 0,
skipped: int = 0,
failed: int = 0,
errors: int = 0,
xpassed: int = 0,
xfailed: int = 0,
) -> None:
"""Compatibility function for result.assert_outcomes"""
result.assert_outcomes(
error=errors, # Pytest < 6 uses the singular form
passed=passed,
skipped=skipped,
failed=failed,
xpassed=xpassed,
xfailed=xfailed,
) # type: ignore[call-arg]
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest_plugins = "pytester"
63 changes: 63 additions & 0 deletions tests/test_factory_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import factory

from pytest_factoryboy import register
from tests.compat import assert_outcomes


@register
class JSONPayloadFactory(factory.Factory):
class Meta:
model = dict

name = "John Doe"


def test_fixture_name_as_expected(json_payload):
"""Test that the json_payload fixture is registered from the JSONPayloadFactory."""
assert json_payload["name"] == "John Doe"


def test_fixture_name_cant_be_determined(testdir):
"""Test that an error is raised if the fixture name can't be determined."""
testdir.makepyfile(
"""
import factory
from pytest_factoryboy import register
@register
class JSONPayloadF(factory.Factory):
class Meta:
model = dict
name = "John Doe"
"""
)
res = testdir.runpytest()
assert_outcomes(res, errors=1)
res.stdout.fnmatch_lines("*JSONPayloadF *does not follow*naming convention*")


def test_invalid_factory_name_override(testdir):
"""Test that, although the factory name doesn't follow the naming convention, it can still be overridden."""
testdir.makepyfile(
"""
import factory
from pytest_factoryboy import register
@register(_name="payload")
class JSONPayloadF(factory.Factory):
class Meta:
model = dict
name = "John Doe"
def test_payload(payload):
assert payload["name"] == "John Doe"
"""
)
res = testdir.runpytest()
assert_outcomes(res, passed=1)

0 comments on commit 56dab10

Please sign in to comment.