如何在 Pytest 中为 Django 模型提供测试Fixtures

举报
Yuchuan 发表于 2021/12/24 19:53:25 2021/12/24
【摘要】 您已经成功实现了一个提供 Django 模型实例的夹具工厂。您还以某种方式维护和实现了固定装置之间的依赖关系,从而减少了编写和维护测试的麻烦。 在本教程中,您学习了: 如何在 Django 中创建和加载设备 如何提供对Django模型测试夹具中pytest 如何使用工厂为 Django 模型创建夹具pytest 如何将工厂实现为夹具模式以在测试夹具之间创建依赖关系

目录

如果你在Django 中工作,pytestfixtures可以帮助你为你的模型创建测试,这些测试维护起来并不复杂。编写好的测试是维持一个成功的应用程序的关键一步,而夹具是使您的测试套件高效和有效的关键因素。Fixtures 是用作测试基线的小数据块。

随着您的测试场景发生变化,添加、修改和维护您的装置可能会很痛苦。不过别担心。本教程将向您展示如何使用pytest-django插件轻松编写新的测试用例和装置。

在本教程中,您将学习:

  • 如何在 Django 中创建和加载测试fixtures
  • 如何为 Django 模型创建和加载pytestfixtures
  • 如何使用工厂为 Django 模型创建测试fixturespytest
  • 如何使用工厂作为fixtures模式在测试夹具之间创建依赖关系

本教程中描述的概念适用于任何使用pytest. 为方便起见,示例使用 Django ORM,但结果可以在其他类型的 ORM 中重现,甚至可以在不使用 ORM 或数据库的项目中重现。

Django 中的固定Fixtures

首先,您将设置一个新的 Django 项目。在本教程中,您将使用内置身份验证模块编写一些测试。

设置 Python 虚拟环境

创建新项目时,最好同时为其创建一个虚拟环境。虚拟环境允许您将项目与计算机上的其他项目隔离开来。这样,不同的项目可以使用不同版本的 Python、Django 或任何其他包,而不会相互干扰。

以下是在新目录中创建虚拟环境的方法:

$ mkdir django_fixtures
$ cd django_fixtures
django_fixtures $ python -m venv venv

有关如何创建虚拟环境的分步说明,请查看Python 虚拟环境:入门

运行此命令将创建一个名为venv. 该目录将存储您在虚拟环境中安装的所有软件包。

设置 Django 项目

现在您有了一个全新的虚拟环境,是时候设置 Django 项目了。在您的终端中,激活虚拟环境并安装 Django:

$ source venv/bin/activate
$ pip install django

现在您已经安装了 Django,您可以创建一个名为 的新 Django 项目django_fixtures

$ django-admin startproject django_fixtures

运行此命令后,您将看到 Django 创建了新文件和目录。有关如何启动新 Django 项目的更多信息,请查看启动 Django 项目

要完成 Django 项目的设置,请应用内置模块的迁移

$ cd django_fixtures
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK

输出列出了 Django 应用的所有迁移。当开始一个新的项目,Django的应用迁移的内置应用程序,如authsessionsadmin

现在您已准备好开始编写测试和装置!

创建 Django Fixture

Django 提供了自己的方式来为文件中的模型创建和加载装置。Django 固定文件可以用JSON或 YAML编写。在本教程中,您将使用 JSON 格式。

创建 Django 固定装置的最简单方法是使用现有对象。启动一个 Django shell:

$ python manage.py shell
Python 3.8.0 (default, Oct 23 2019, 18:51:26)
[GCC 9.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)

在 Django shell 中,创建一个名为 的新组appusers

>>>
>>> from django.contrib.auth.models import Group
>>> group = Group.objects.create(name="appusers")
>>> group.pk
1

Group模型是Django 认证系统的一部分。组对于管理 Django 项目中的权限非常有用。

您创建了一个名为 的新组appusers。您刚刚创建的组的主键1。要为组创建一个装置appusers,您将使用Django 管理命令dumpdata

退出 Django shellexit()并从终端执行以下命令:

$ python manage.py dumpdata auth.Group --pk 1 --indent 4 > group.json

在本例中,您使用该dumpdata命令从现有模型实例生成夹具文件。让我们分解一下:

  • auth.Group:这描述了要转储的模型。格式为<app_label>.<model_name>.

  • --pk 1:这描述了要转储的对象。该值是以逗号分隔的主键列表,例如1,2,3.

  • --indent 4:这是一个可选的格式化参数,它告诉 Django 在生成的文件中的每个缩进级别之前添加多少空格。使用缩进使夹具文件更具可读性。

  • > group.json:这描述了在哪里写入命令的输出。在这种情况下,输出将被写入一个名为group.json.

接下来,检查夹具文件的内容group.json

[
{
    "model": "auth.group",
    "pk": 1,
    "fields": {
        "name": "appusers",
        "permissions": []
    }
}
]

夹具文件包含一个对象列表。在这种情况下,列表中只有一个对象。每个对象都包含一个带有模型名称和主键的标头,以及一个带有模型中每个字段值的字典。您可以看到夹具包含组的名称appusers

您可以手动创建和编辑fixt​​ure 文件,但通常预先创建对象并使用Django 的dumpdata命令创建fixture 文件更方便。

加载 Django Fixtures

现在您有一个夹具文件,您想将它加载到数据库中。但在此之前,您应该打开一个 Django shell 并删除您已经创建的组:

>>>
>>> from django.contrib.auth.models import Group
>>> Group.objects.filter(pk=1).delete()
(1, {'auth.Group_permissions': 0, 'auth.User_groups': 0, 'auth.Group': 1})

现在该组已被删除,请使用以下loaddata命令加载夹具:

$ python manage.py loaddata group.json
Installed 1 object(s) from 1 fixture(s)

要确保加载了新组,请打开 Django shell 并获取它:

>>>
>>> from django.contrib.auth.models import Group
>>> group = Group.objects.get(pk=1)
>>> vars(group)
{'_state': <django.db.models.base.ModelState at 0x7f3a012d08b0>,
 'id': 1,
 'name': 'appusers'}

伟大的!该组已加载。您刚刚创建并加载了您的第一个 Django 夹具。

在测试中加载 Django Fixtures

到目前为止,您已经从命令行创建并加载了一个夹具文件。现在如何使用它进行测试?要查看在 Django 测试中如何使用夹具,请创建一个名为 的新文件test.py,并添加以下测试:

from django.test import TestCase
from django.contrib.auth.models import Group

class MyTest(TestCase):
    def test_should_create_group(self):
        group = Group.objects.get(pk=1)
        self.assertEqual(group.name, "appusers")

该测试正在使用主键获取组1并测试其名称是否为appusers

从终端运行测试:

$ python manage.py test test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_should_create_group (test.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/django_fixtures/django_fixtures/test.py", line 9, in test_should_create_group
    group = Group.objects.get(pk=1)
  File "/django_fixtures/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/django_fixtures/venv/lib/python3.8/site-packages/django/db/models/query.py", line 415, in get
    raise self.model.DoesNotExist(
django.contrib.auth.models.Group.DoesNotExist: Group matching query does not exist.

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)
Destroying test database for alias 'default'...

测试失败,因为具有主键的组1不存在。

要在测试中加载夹具,您可以使用名为的类的特殊属性TestCasefixtures

from django.test import TestCase
from django.contrib.auth.models import Group

class MyTest(TestCase):
    fixtures = ["group.json"]

    def test_should_create_group(self):
        group = Group.objects.get(pk=1)
        self.assertEqual(group.name, "appusers")

将此属性添加到 aTestCase告诉 Django 在执行每个测试之前加载夹具。请注意,它fixtures接受一个数组,因此您可以在每次测试之前提供要加载的多个夹具文件。

现在运行测试会产生以下输出:

$ python manage.py test test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.005s

OK
Destroying test database for alias 'default'...

惊人!该组已加载并通过测试。您现在可以appusers在测试中使用该组。

在 Django Fixtures 中引用相关对象

到目前为止,您只使用了一个文件和一个对象。但是,大多数情况下,您的应用程序中会有许多模型,并且在测试中需要多个模型。

要查看对象之间的依赖关系在 Django 固定装置中的外观,请创建一个新的用户实例,然后将其添加到appusers您之前创建的组中:

>>>
>>> from django.contrib.auth.models import User, Group
>>> appusers = Group.objects.get(name="appusers")
>>> haki = User.objects.create_user("haki")
>>> haki.pk
1
>>> haki.groups.add(appusers)

用户haki现在是该appusers组的成员。要查看带有外键的夹具是什么样子,请为 user 生成一个夹具1

$ python manage.py dumpdata auth.User --pk 1 --indent 4
[
{
    "model": "auth.user",
    "pk": 1,
    "fields": {
        "password": "!M4dygH3ZWfd0214U59OR9nlwsRJ94HUZtvQciG8y",
        "last_login": null,
        "is_superuser": false,
        "username": "haki",
        "first_name": "",
        "last_name": "",
        "email": "",
        "is_staff": false,
        "is_active": true,
        "date_joined": "2019-12-07T09:32:50.998Z",
        "groups": [
            1
        ],
        "user_permissions": []
    }
}
]

夹具的结构与您之前看到的结构相似。

一个用户可以与多个组相关联,因此该字段group包含该用户所属的所有组的 ID。在这种情况下,用户属于具有主键的组1,即您的appusers组。

使用主键来引用装置中的对象并不总是一个好主意。组的主键是数据库在创建组时分配给该组的任意标识符。在另一个环境或另一台计算机上,该appusers组可以具有不同的 ID,并且不会对对象产生任何影响。

为了避免使用任意标识符,Django 定义了自然键的概念。自然键是对象的唯一标识符,不一定是主键。在组的情况下,两个组不能有相同的名称,因此组的自然键可以是它的名称。

要使用自然键而不是主键来引用 Django 固定装置中的相关对象,请将--natural-foreign标志添加到dumpdata命令中:

$ python manage.py dumpdata auth.User --pk 1 --indent 4 --natural-foreign
[
{
    "model": "auth.user",
    "pk": 1,
    "fields": {
        "password": "!f4dygH3ZWfd0214X59OR9ndwsRJ94HUZ6vQciG8y",
        "last_login": null,
        "is_superuser": false,
        "username": "haki",
        "first_name": "",
        "last_name": "",
        "email": "benita",
        "is_staff": false,
        "is_active": true,
        "date_joined": "2019-12-07T09:32:50.998Z",
        "groups": [
            [
                `appusers`
            ]
        ],
        "user_permissions": []
    }
}
]

Django 为用户生成了fixture,但appusers它没有使用组的主键,而是使用了组的名称。

您还可以添加--natural-primary标志以从夹具中排除对象的主键。当pk为 null 时,主键将在运行时设置,通常由数据库设置。

维护 Django Fixtures

Django Fixture 很棒,但它们也带来了一些挑战:

  • 保持

    Fixtures

    更新:Django 装置必须包含模型的所有必需字段。如果添加不可为空的新字段,则必须更新设备。否则,它们将无法加载。当你有很多 Django 设备时,保持更新会成为一种负担。

  • 维护设备之间的依赖关系:依赖于其他设备的 Django 设备必须以特定顺序一起加载。在添加新测试用例和修改旧测试用例时跟上固定装置可能具有挑战性。

由于这些原因,Django Fixtures 不是经常变化的模型的理想选择。例如,对于用于表示应用程序中核心对象(如销售、订单、交易或预订)的模型,维护 Django 固定装置将非常困难。

另一方面,Django 设备是以下用例的绝佳选择:

  • 常量数据:这适用于很少更改的模型,例如国家/地区代码和邮政编码。

  • 初始数据:这适用于存储应用查找数据的模型,例如产品类别、用户组和用户类型。

pytest Django 中的固定 Fixtures

在上一节中,您使用了 Django 提供的内置工具来创建和加载设备。Django 提供的装置非常适合某些用例,但不适合其他用例。

在本节中,您将尝试使用一种非常不同的夹具:pytest夹具。pytest提供了一个非常广泛的夹具系统,您可以使用它来创建一个可靠且可维护的测试套件。

设置pytest为Django项目

要开始使用pytest,您首先需要安装pytestDjango的插件pytest。激活虚拟环境时,在终端中执行以下命令:

$ pip install pytest
$ pip install pytest-django

pytest-django插件由pytest开发团队维护。它为使用pytest.

接下来,您需要pytest知道它可以在哪里找到您的 Django 项目设置。在项目的根目录中创建一个名为 的新文件pytest.ini,并向其中添加以下几行:

[pytest]
DJANGO_SETTINGS_MODULE=django_fixtures.settings

这是pytest使用 Django 项目所需的最少配置。还有更多的配置选项,但这足以开始。

最后,为了测试你的设置,test.py用这个虚拟测试替换 的内容:

def test_foo():
    assert True

要运行虚拟测试,请使用pytest终端中的命令:

$ pytest test.py
============================== test session starts ======================
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1

test.py .
                                [100%]
============================= 1 passed in 0.05s =========================

您刚刚完成了一个新的 Django 项目的设置pytest!现在您已准备好深入挖掘。

有关如何设置pytest和编写测试的更多信息,请查看使用pytest.

从测试访问数据库

在本节中,您将使用内置身份验证模块编写测试django.contrib.auth。本模块中最熟悉的模型是UserGroup

要开始使用 Django 和pytest,请编写一个测试来检查create_user()Django 提供的函数是否正确设置了用户名:

from django.contrib.auth.models import User

def test_should_create_user_with_username() -> None:
    user = User.objects.create_user("Haki")
    assert user.username == "Haki"

现在,尝试从您的命令执行测试,例如:

$ pytest test.py
================================== test session starts ===============
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django-django_fixtures/django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py F

=============================== FAILURES =============================
____________________test_should_create_user_with_username ____________

    def test_should_create_user_with_username() -> None:
>       user = User.objects.create_user("Haki")

self = <mydbengine.base.DatabaseWrapper object at 0x7fef66ed57d0>, name = None

    def _cursor(self, name=None):
>       self.ensure_connection()

E   Failed: Database access not allowed, use the "django_db" mark, or the "db"
        or "transactional_db" fixtures to enable it.

命令失败,测试未执行。错误消息为您提供了一些有用的信息: 要在测试中访问数据库,您需要注入一个名为db. 该db装置是django-pytest您之前安装的插件的一部分,在测试中访问数据库需要它。

db夹具注入测试:

from django.contrib.auth.models import User

def test_should_create_user_with_username(db) -> None:
    user = User.objects.create_user("Haki")
    assert user.username == "Haki"

再次运行测试:

$ pytest test.py
================================== test session starts ===============
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py .

伟大的!命令成功完成,您的测试通过。您现在知道如何在测试中访问数据库。在此过程中,您还向测试用例中注入了一个装置。

为 Django 模型创建Fixtures

既然您已经熟悉了 Django 和pytest,请编写一个测试来检查设置的密码set_password()是否按预期进行了验证。test.py用这个测试替换的内容:

from django.contrib.auth.models import User

def test_should_check_password(db) -> None:
    user = User.objects.create_user("A")
    user.set_password("secret")
    assert user.check_password("secret") is True

def test_should_not_check_unusable_password(db) -> None:
    user = User.objects.create_user("A")
    user.set_password("secret")
    user.set_unusable_password()
    assert user.check_password("secret") is False

第一个测试检查具有可用密码的用户是否正在被 Django 验证。第二个测试检查用户密码不可用且不应由 Django 验证的边缘情况。

这里有一个重要的区别:上面的测试用例不测试create_user(). 他们测试set_password()。这意味着更改create_user()不应影响这些测试用例。

另外,请注意User实例被创建了两次,每个测试用例一次。一个大型项目可以有许多需要一个User实例的测试。如果每个测试用例都会创建自己的用户,那么如果User模型发生变化,您将来可能会遇到麻烦。

要在许多测试用例中重用对象,您可以创建一个测试Fixtures

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

@pytest.fixture
def user_A(db) -> User:
    return User.objects.create_user("A")

def test_should_check_password(db, user_A: User) -> None:
    user_A.set_password("secret")
    assert user_A.check_password("secret") is True

def test_should_not_check_unusable_password(db, user_A: User) -> None:
    user_A.set_password("secret")
    user_A.set_unusable_password()
    assert user_A.check_password("secret") is False

在上面的代码中,您创建了一个名为的函数user_A(),用于创建并返回一个新User实例。要将函数标记为固定装置,您可以使用pytest.fixture装饰器装饰它。一旦一个函数被标记为一个fixture,它就可以被注入到测试用例中。在本例中,您将夹具user_A注入到两个测试用例中。

需求变化时维护Fixtures

假设您向应用程序添加了一个新需求,现在每个用户都必须属于一个特殊"app_user"组。该组中的用户可以查看和更新​​他们自己的个人详细信息。要测试您的应用程序,您需要您的测试用户也属于该"app_user"组:

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def user_A(db) -> Group:
    group = Group.objects.create(name="app_user")
    change_user_permissions = Permission.objects.filter(
        codename__in=["change_user", "view_user"],
    )
    group.permissions.add(*change_user_permissions)
    user = User.objects.create_user("A")
    user.groups.add(group)
    return user

def test_should_create_user(user_A: User) -> None:
    assert user_A.username == "A"

def test_user_is_in_app_user_group(user_A: User) -> None:
    assert user_A.groups.filter(name="app_user").exists()

灯具内部创建的组"app_user",并添加相关change_userview_user权限的话。然后您创建了测试用户并将其添加到"app_user"组中。

以前,您需要查看创建用户的每个测试用例并将其添加到组中。使用夹具,您只能进行一次更改。一旦您更改了夹具,您注入的每个测试用例中都会出现相同的更改user_A使用fixtures,您可以避免重复并使您的测试更易于维护。

将夹具注入其他Fixtures

大型应用程序通常不止一个用户,而且经常需要用多个用户来测试它们。在这种情况下,您可以添加另一个夹具来创建测试user_B

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def user_A(db) -> User:
    group = Group.objects.create(name="app_user")
    change_user_permissions = Permission.objects.filter(
        codename__in=["change_user", "view_user"],
    )
    group.permissions.add(*change_user_permissions)
    user = User.objects.create_user("A")
    user.groups.add(group)
    return user

@pytest.fixture
def user_B(db) -> User:
    group = Group.objects.create(name="app_user")
    change_user_permissions = Permission.objects.filter(
        codename__in=["change_user", "view_user"],
    )
    group.permissions.add(*change_user_permissions)
    user = User.objects.create_user("B")
    user.groups.add(group)
    return user

def test_should_create_two_users(user_A: User, user_B: User) -> None:
    assert user_A.pk != user_B.pk

在您的终端中,尝试运行测试:

$ pytest test.py
==================== test session starts =================================
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py E
                              [100%]
============================= ERRORS ======================================
_____________ ERROR at setup of test_should_create_two_users ______________

self = <django.db.backends.utils.CursorWrapper object at 0x7fc6ad1df210>,
sql ='INSERT INTO "auth_group" ("name") VALUES (%s) RETURNING "auth_group"."id"'
,params = ('app_user',)

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                # params default might be backend specific.
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               psycopg2.IntegrityError: duplicate key value violates
                unique constraint "auth_group_name_key"
E               DETAIL:  Key (name)=(app_user) already exists.

======================== 1 error in 4.14s ================================

新测试抛出一个IntegrityError. 错误消息源自数据库,因此根据您使用的数据库,它可能看起来有些不同。根据错误信息,测试违反了对组名的唯一约束。当您查看您的固定装置时,这是有道理的。该"app_user"组被创建两次,一次在夹具中user_A,一次在夹具中user_B

到目前为止我们忽略了一个有趣的观察结果是夹具user_A正在使用夹具db。这意味着fixtures 可以被注入到其他fixtures 中。您可以使用此功能来解决IntegrityError上述问题。"app_user"在夹具中只创建一次组,并将其注入user_Auser_B夹具。

为此,重构您的测试并添加一个"app user"组夹具:

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def app_user_group(db) -> Group:
    group = Group.objects.create(name="app_user")
    change_user_permissions = Permission.objects.filter(
        codename__in=["change_user", "view_user"],
    )
    group.permissions.add(*change_user_permissions)
    return group

@pytest.fixture
def user_A(db, app_user_group: Group) -> User:
    user = User.objects.create_user("A")
    user.groups.add(app_user_group)
    return user

@pytest.fixture
def user_B(db, app_user_group: Group) -> User:
    user = User.objects.create_user("B")
    user.groups.add(app_user_group)
    return user

def test_should_create_two_users(user_A: User, user_B: User) -> None:
    assert user_A.pk != user_B.pk

在您的终端中,运行您的测试:

$ pytest test.py
================================== test session starts ===============
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py .

惊人!你的测试通过了。group fixture 封装了与"app user"group相关的逻辑,比如设置权限。然后,您将该组注入到两个单独的用户设备中。通过以这种方式构建您的装置,您可以让您的测试更易于阅读和维护。

使用工厂

到目前为止,您已经创建了具有很少参数的对象。但是,某些对象可能更复杂,具有许多参数和许多可能的值。对于此类对象,您可能希望创建多个测试装置。

例如,如果您向 提供所有参数create_user(),则夹具的外观如下:

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

@pytest.fixture
def user_A(db, app_user_group: Group) -> User
    user = User.objects.create_user(
        username="A",
        password="secret",
        first_name="haki",
        last_name="benita",
        email="me@hakibenita.com",
        is_staff=False,
        is_superuser=False,
        is_active=True,
    )
    user.groups.add(app_user_group)
    return user

你的装置变得更复杂了!用户实例现在可以有许多不同的变体,例如超级用户、员工用户、非活动员工用户和非活动普通用户。

在前面的部分中,您了解到在每个测试夹具中维护复杂的设置逻辑可能很困难。因此,为了避免每次创建用户时都必须重复所有值,请添加一个用于create_user()根据应用的特定需求创建用户的函数:

from typing import List, Optional
from django.contrib.auth.models import User, Group

def create_app_user(
    username: str,
    password: Optional[str] = None,
    first_name: Optional[str] = "first name",
    last_name: Optional[str] = "last name",
    email: Optional[str] = "foo@bar.com",
    is_staff: str = False,
    is_superuser: str = False,
    is_active: str = True,
    groups: List[Group] = [],
) -> User:
    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,
    )
    user.groups.add(*groups)
    return user

该函数创建一个应用程序用户。根据您的应用程序的特定要求,每个参数都设置了合理的默认值。例如,您的应用程序可能要求每个用户都有一个电子邮件地址,但 Django 的内置函数并没有强制执行这样的限制。您可以改为在您的函数中强制执行该要求。

创建对象的函数和类通常称为工厂。为什么?这是因为这些函数充当生成特定类实例的工厂。有关 Python 工厂的更多信息,请查看工厂方法模式及其在 Python 中的实现

上面的函数是一个工厂的直接实现。它没有状态,也没有实现任何复杂的逻辑。您可以重构您的测试,以便它们使用工厂函数在您的装置中创建用户实例:

@pytest.fixture
def user_A(db, app_user_group: Group) -> User:
    return create_user(username="A", groups=[app_user_group])

@pytest.fixture
def user_B(db, app_user_group: Group) -> User:
    return create_user(username="B", groups=[app_user_group])

def test_should_create_user(user_A: User, app_user_group: Group) -> None:
    assert user_A.username == "A"
    assert user_A.email == "foo@bar.com"
    assert user_A.groups.filter(pk=app_user_group.pk).exists()

def test_should_create_two_users(user_A: User, user_B: User) -> None:
    assert user_A.pk != user_B.pk

您的装置变短了,您的测试现在更能适应变化。例如,如果您使用自定义用户模型并且您刚刚向模型添加了一个新字段,则您只需要更改即可create_user()让您的测试按预期工作。

使用工厂作为Fixtures

复杂的设置逻辑使编写和维护测试变得更加困难,从而使整个套件变得脆弱并且对更改的弹性降低。到目前为止,您已经通过创建夹具、在夹具之间创建依赖关系以及使用工厂来尽可能多地抽象设置逻辑来解决这个问题。

但是您的测试 Fixtures 中仍然存在一些设置逻辑:

@pytest.fixture
def user_A(db, app_user_group: Group) -> User:
    return create_user(username="A", groups=[app_user_group])

@pytest.fixture
def user_B(db, app_user_group: Group) -> User:
    return create_user(username="B", groups=[app_user_group])

两个夹具都注入了app_user_group. 这是当前必需的,因为工厂函数create_user()无法访问app_user_group夹具。在每个测试中都有这个设置逻辑会使更改变得更加困难,并且在未来的测试中更有可能被忽略。相反,您希望封装创建用户的整个过程并将其从测试中抽象出来。这样,您就可以专注于手头的场景,而不是设置独特的测试数据。

要为用户工厂提供对app_user_group夹具的访问权限,您可以使用称为factory as fixture的模式:

from typing import List, Optional

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def app_user_group(db) -> Group:
    group = Group.objects.create(name="app_user")
    change_user_permissions = Permission.objects.filter(
        codename__in=["change_user", "view_user"],
    )
    group.permissions.add(*change_user_permissions)
    return group

@pytest.fixture
def app_user_factory(db, app_user_group: Group):
    # Closure
    def create_app_user(
        username: str,
        password: Optional[str] = None,
        first_name: Optional[str] = "first name",
        last_name: Optional[str] = "last name",
        email: Optional[str] = "foo@bar.com",
        is_staff: str = False,
        is_superuser: str = False,
        is_active: str = True,
        groups: List[Group] = [],
    ) -> User:
        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,
        )
        user.groups.add(app_user_group)
        # Add additional groups, if provided.
        user.groups.add(*groups)
        return user
    return create_app_user

这与您已经完成的工作相距不远,所以让我们分解一下:

  • app_user_group夹具保持不变。它创建"app user"具有所有必要权限的特殊组。

  • 添加了一个名为的新夹具app_user_factory,并与app_user_group夹具一起注入。

  • 该夹具app_user_factory创建一个闭包并返回一个名为的内部函数create_app_user()

  • create_app_user()与您之前实现的功能类似,但现在它可以访问 fixture app_user_group。通过访问组,您现在可以app_user_group在工厂功能中添加用户。

要使用app_user_factory夹具,请将其注入另一个夹具并使用它来创建用户实例:

@pytest.fixture
def user_A(db, app_user_factory) -> User:
    return app_user_factory("A")

@pytest.fixture
def user_B(db, app_user_factory) -> User:
    return app_user_factory("B")

def test_should_create_user_in_app_user_group(
    user_A: User,
    app_user_group: Group,
) -> None:
    assert user_A.groups.filter(pk=app_user_group.pk).exists()

def test_should_create_two_users(user_A: User, user_B: User) -> None:
    assert user_A.pk != user_B.pk

请注意,与以前不同,您创建的夹具提供的是函数而不是对象。这是作为夹具模式的工厂背后的主要概念:工厂 Fixtures 创建了一个闭包,它为内部函数提供了对夹具的访问。

有关 Python 中的闭包的更多信息,请查看Python 内部函数 — 它们有什么用?

现在你有了你的工厂和Fixtures,这是你测试的完整代码:

from typing import List, Optional

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def app_user_group(db) -> Group:
    group = Group.objects.create(name="app_user")
    change_user_permissions = Permission.objects.filter(
        codename__in=["change_user", "view_user"],
    )
    group.permissions.add(*change_user_permissions)
    return group

@pytest.fixture
def app_user_factory(db, app_user_group: Group):
    # Closure
    def create_app_user(
        username: str,
        password: Optional[str] = None,
        first_name: Optional[str] = "first name",
        last_name: Optional[str] = "last name",
        email: Optional[str] = "foo@bar.com",
        is_staff: str = False,
        is_superuser: str = False,
        is_active: str = True,
        groups: List[Group] = [],
    ) -> User:
        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,
        )
        user.groups.add(app_user_group)
        # Add additional groups, if provided.
        user.groups.add(*groups)
        return user
    return create_app_user

@pytest.fixture
def user_A(db, app_user_factory) -> User:
    return app_user_factory("A")

@pytest.fixture
def user_B(db, app_user_factory) -> User:
    return app_user_factory("B")

def test_should_create_user_in_app_user_group(
    user_A: User,
    app_user_group: Group,
) -> None:
    assert user_A.groups.filter(pk=app_user_group.pk).exists()

def test_should_create_two_users(user_A: User, user_B: User) -> None:
    assert user_A.pk != user_B.pk

打开终端并运行测试:

$ pytest test.py
======================== test session starts ========================
platform linux -- Python 3.8.1, pytest-5.3.3, py-1.8.1, pluggy-0.13.1
django: settings: django_fixtures.settings (from ini)
rootdir: /django_fixtures/django_fixtures, inifile: pytest.ini
plugins: django-3.8.0
collected 2 items

test.py ..                                                     [100%]

======================== 2 passed in 0.17s ==========================

很好!您已经在测试中成功地将工厂实现为夹具模式。

工厂作为实践中的 Fixtures

工厂作为夹具模式非常有用。非常有用,事实上,你可以在它pytest自己提供的装置中找到它。例如,tmp_path提供的夹具pytest是由夹具工厂创建的tmp_path_factory。同样,tmpdir夹具是由夹具工厂创建的tmpdir_factory

掌握工厂作为夹具模式可以消除许多与编写和维护测试相关的麻烦。

结论

您已经成功实现了一个提供 Django 模型实例的夹具工厂。您还以某种方式维护和实现了固定装置之间的依赖关系,从而减少了编写和维护测试的麻烦。

在本教程中,您学习了:

  • 如何在 Django 中创建和加载设备
  • 如何提供对Django模型测试

    Fixtures

    pytest
  • 如何使用工厂为 Django 模型创建夹具pytest
  • 如何将工厂实现

    Fixtures

    模式以在测试夹具之间创建依赖关系

您现在可以实施和维护可靠的测试套件,帮助您更快地生成更好、更可靠的代码!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。