[Django Rest Framework] 使用 Pytest 來編寫 django-filter 的測試吧!

简要概述

我将解释如何使用pytest编写django-filter的测试代码。

前提 – tí
条件 –
假设 – Jiǎ shè

只需要一个选项 – Zhǐ xū yī gè

Please let me know if you need any further assistance.

    • Django Rest Frameworkのプロジェクトを作成済み

 

    django-filterをインストール済み

目录结构

tree
・
└── application
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── filters.py
    ├── fixtures
    │   └── fixture.json
    ├── models.py
    ├── serializers.py
    ├── tests
    │   ├── __init__.py
    │   ├── factories
    │   │   └── factory.py
    │   └── test_filters.py
    ├── urls.py
    └── views.py

首先

在创建测试代码之前,请先创建以下内容。

    • model

 

    • fixture

 

    • factoryboy

 

    • filter

 

    • serializer

 

    view

创建模型

只需要一个选择,以下是用中文本地化的释义:

这次

    • User

 

    • Customer

 

    Address

我会创建一个model。

import uuid

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.core.validators import RegexValidator
from django.db import models


class User(AbstractUser):
    """システムユーザ"""

    username_validator = UnicodeUsernameValidator()

    class Role(models.IntegerChoices):
        """システムユーザのロール

        Args:
            MANAGEMENT(0): 管理者
            GENERAL(1):    一般
            PART_TIME(2):  アルバイト
        """

        MANAGEMENT = 0, "管理者"
        GENERAL = 1, "一般"
        PART_TIME = 2, "アルバイト"

    # 不要なフィールドはNoneにすることができる
    first_name = None
    last_name = None
    date_joined = None
    groups = None
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
        db_comment="システムユーザID",
    )
    employee_number = models.CharField(
        unique=True,
        validators=[RegexValidator(r"^[0-9]{8}$")],
        max_length=8,
        db_comment="社員番号",
    )
    username = models.CharField(
        max_length=150,
        unique=True,
        validators=[username_validator],
        db_comment="ユーザ名",
    )
    email = models.EmailField(
        max_length=254,
        unique=True,
        db_comment="メールアドレス",
    )
    role = models.PositiveIntegerField(
        choices=Role.choices,
        default=Role.PART_TIME,
        db_comment="システムユーザのロール",
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        db_comment="作成日",
    )
    updated_at = models.DateTimeField(
        auto_now=True,
        db_comment="更新日",
    )
    is_verified = models.BooleanField(
        default=False,
        db_comment="有効化有無",
    )

    USERNAME_FIELD = "employee_number"
    REQUIRED_FIELDS = ["email", "username"]

    class Meta:
        ordering = ["employee_number"]
        db_table = "User"
        db_table_comment = "システムユーザ"

    def __str__(self):
        return self.username


class Customer(models.Model):
    """お客様"""

    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
        db_comment="ID",
    )
    kana = models.CharField(
        max_length=255,
        db_comment="カナ氏名",
    )
    name = models.CharField(
        max_length=255,
        db_comment="氏名",
    )
    birthday = models.DateField(
        db_comment="誕生日",
    )
    email = models.EmailField(
        db_comment="メールアドレス",
    )
    phone_no = models.CharField(
        max_length=11,
        validators=[RegexValidator(r"^[0-9]{11}$", "11桁の数字を入力してください。")],
        blank=True,
        db_comment="電話番号",
    )
    address = models.OneToOneField(
        "Address",
        on_delete=models.CASCADE,
        related_name="address",
        db_comment="住所のFK",
    )

    class Meta:
        db_table = "Customer"


class Address(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
        db_comment="ID",
    )
    prefecture = models.CharField(
        max_length=255,
        db_comment="都道府県",
    )
    municipalities = models.CharField(
        max_length=255,
        db_comment="市区町村",
    )
    house_no = models.CharField(
        max_length=255,
        db_comment="丁・番地",
    )
    other = models.CharField(
        max_length=255,
        blank=True,
        db_comment="その他(マンション名など)",
    )
    post_no = models.CharField(
        max_length=7,
        validators=[RegexValidator(r"^[0-9]{7}$", "7桁の数字を入力してください。")],
        null=True,
        db_comment="郵便番号",
    )

    class Meta:
        db_table = "Address"        

创建固定装置

创建模型的测试数据(fixture)。

[
    {
        "model": "application.User",
        "pk": 1,
        "fields": {
            "employee_number": "00000001",
            "username": "test01",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test01@example.com",
            "role": 0,
            "is_superuser": 0,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "application.User",
        "pk": 2,
        "fields": {
            "employee_number": "00000002",
            "username": "test02",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test02@example.com",
            "role": 1,
            "is_superuser": 0,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "application.User",
        "pk": 3,
        "fields": {
            "employee_number": "00000003",
            "username": "test03",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test03@example.com",
            "role": 2,
            "is_superuser": 0,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "application.User",
        "pk": 4,
        "fields": {
            "employee_number": "00000004",
            "username": "test04",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test04@example.com",
            "role": 0,
            "is_superuser": 1,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "application.Customer",
        "pk": 1,
        "fields": {
            "kana": "オオサカタロウ",
            "name": "大阪太郎",
            "birthday": "1992-01-06",
            "email":"osaka@example.com",
            "phone_no": "08011112222",
            "address": 1
        }
    },
    {
        "model": "application.Customer",
        "pk": 2,
        "fields": {
            "kana": "キョウトジロウ",
            "name": "京都二郎",
            "birthday": "1994-01-06",
            "email":"kyoto@example.com",
            "phone_no": "08022223333",
            "address": 2
        }
    },
    {
        "model": "application.Customer",
        "pk": 3,
        "fields": {
            "kana": "ヒョウゴサブロウ",
            "name": "兵庫三郎",
            "birthday": "1995-03-06",
            "email":"hyogo@example.com",
            "phone_no": "08033334444",
            "address": 3
        }
    },
    {
        "model": "application.Address",
        "pk": 1,
        "fields": {
            "prefecture": "京都府",
            "municipalities": "京都市東山区",
            "house_no": "清水",
            "other": "1-294",
            "post_no": "6050862"
        }
    },
    {
        "model": "application.Address",
        "pk": 2,
        "fields": {
            "prefecture": "京都府",
            "municipalities": "京都市東山区",
            "house_no": "北区金閣寺町1",
            "other": "",
            "post_no": "6038361"
        }
    },
    {
        "model": "application.Address",
        "pk": 3,
        "fields": {
            "prefecture": "京都府",
            "municipalities": "京都市東山区",
            "house_no": "左京区銀閣寺町2",
            "other": "",
            "post_no": "6068402"
        }
    }
]

创建工厂

from datetime import datetime, timedelta

from factory import Faker, PostGenerationMethodCall, Sequence, SubFactory
from factory.django import DjangoModelFactory

from application.models import Address, Customer, User


class UserFactory(DjangoModelFactory):
    class Meta:
        model = User

    username = Sequence(lambda n: "テスト利用者{}".format(n))
    employee_number = Sequence(lambda n: "{0:08}".format(n + 100))
    password = PostGenerationMethodCall("set_password", "test")
    email = Faker("email")
    role = Faker(
        "random_int",
        min=0,
        max=2,
    )
    created_at = Faker(
        "date_between_dates",
        date_start=(datetime.now() - timedelta(days=20)).date(),
        date_end=datetime.now(),
    )
    updated_at = Faker(
        "date_between_dates",
        date_start=(datetime.now() - timedelta(days=20)).date(),
        date_end=datetime.now(),
    )
    is_verified = True


class AddressFactory(DjangoModelFactory):
    class Meta:
        model = Address

    prefecture = Faker("administrative_unit", locale="ja_JP")
    municipalities = Faker("city", locale="ja_JP")
    house_no = str(Faker("ban", locale="ja_JP")) + str(
        Faker("gou", locale="ja_JP")
    )
    other = str(Faker("building_name", locale="ja_JP")) + str(
        Faker("building_number", locale="ja_JP")
    )
    post_no = Faker("random_number", digits=7)


class CustomerFactory(DjangoModelFactory):
    class Meta:
        model = Customer

    kana = Sequence(lambda n: "テストコキャク{}".format(n))
    name = Sequence(lambda n: "テスト顧客{}".format(n))
    birthday = Faker(
        "date_between_dates",
        date_start=(datetime.now().date() - timedelta(days=365 * 50)),
        date_end=(datetime.now().date() - timedelta(days=365 * 20)),
    )
    email = Faker("email")
    phone_no = Sequence(lambda n: f"080" + "{0:08}".format(n + 100))
    address = SubFactory(AddressFactory)

创建筛选器

我们将使用django_filters创建过滤器。
我们创建的过滤器如下所示。

modelfield絞り込みUsercreated_at範囲指定
username部分一致
email部分一致
role複数選択Customername部分一致
address部分一致
name部分一致
birthday完全一致
email部分一致
phone_no部分一致(前方一致)
import django_filters
from application.models import Customer, User
from django.db.models import Q
from django.db.models.functions import Concat


class UserFilter(django_filters.FilterSet):
    """システムユーザのfilter"""
    
    created_at = django_filters.DateTimeFromToRangeFilter()

    class Meta:
        model = User
        fields = {
            "username": ["contains"],
            "email": ["contains"],
            "role": ["in"],
        }


class CustomerFilter(django_filters.FilterSet):
    """お客様のfilter"""
    
    name = django_filters.CharFilter(method="search_name")
    address = django_filters.CharFilter(method="search_address")

    class Meta:
        model = Customer
        fields = {
            "birthday": ["exact"],
            "email": ["contains"],
            "phone_no": ["startswith"],
        }

    def search_name(self, queryset, name, value):
        """取得した名前に該当するquerysetを取得
        
        Args:
            queryset
            name
            value

        Returns:
            queryset: customerから取得したnameもしくはkanaに該当するqueryset
        """
        return queryset.filter(
            Q(name__contains=value) | Q(kana__contains=value)
        )

    def search_address(self, queryset, address, value):
        """取得した住所に該当するquerysetを取得
        
        Args:
            queryset
            address
        Returns:
            queryset: addressから取得した都道府県・市区町村・番地・その他に該当するqueryset
        """
        return queryset.annotate(
            customer_address=Concat(
                "address__prefecture",
                "address__municipalities",
                "address__house_no",
                "address__post_no",
                "address__other",
            )
        ).filter(customer_address__icontains=value)

创建序列化器

    • User

 

    Customer

创建一个Serializer。

from rest_framework import serializers

from application.models import Customer, User


class UserSerializer(serializers.ModelSerializer):
    """ユーザ用シリアライザ"""

    class Meta:
        model = User
        fields = [
            "id",
            "employee_number",
            "username",
            "email",
            "role",
            "is_verified",
        ]
        read_only_fields = ["id", "created_at", "updated_at"]

    def to_representation(self, instance):
        rep = super(UserSerializer, self).to_representation(instance)
        rep["role"] = instance.get_role_display()
        return rep


class CustomerSerializer(serializers.ModelSerializer):
    """ユーザ用シリアライザ"""

    class Meta:
        model = Customer
        fields = "__all__"
        read_only_fields = ["id"]

    def to_representation(self, instance):
        rep = super(CustomerSerializer, self).to_representation(instance)
        rep["address"] = (
            instance.address.prefecture
            + instance.address.municipalities
            + instance.address.house_no
            + instance.address.other
        )
        rep["post_no"] = instance.address.post_no
        return rep

创建视图

    • User

 

    Customer

我将创建一个ViewSet。

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet

from application.filters import CustomerFilter, UserFilter
from application.models import Customer
from application.serializers.customer import CustomerSerializer, UserSerializer


class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [AllowAny]
    filter_backends = [
        DjangoFilterBackend,
    ]
    filterset_class = UserFilter    


class CustomerViewSet(ModelViewSet):
    queryset = Customer.objects.select_related("address")
    serializer_class = CustomerSerializer
    permission_classes = [AllowAny]
    filter_backends = [
        DjangoFilterBackend,
    ]
    filterset_class = CustomerFilter

让我们创建一个filter的测试吧!

我将在这里进行过滤器的测试。

完全符合

在Filter中放入字段和项目名称。

customer_filter = CustomerFilter({"name": "降田絞込"})

多數一致

当使用{field}__in时,可以通过filter进行多个匹配。

    user_filter = UserFilter(
        {"role__in": f"{User.Role.MANAGEMENT},{User.Role.GENERAL}"}
    )

向前看有统一

当使用{field}__startswith时,可以通过过滤器进行前向匹配。

customer_filter = CustomerFilter({"phone_no__startswith": "01202018"})

测试过滤器.py

在filter的实例内有一个名为qs的数组,根据qs的count和数组顺序来编写测试代码。
源代码如下:

from datetime import timedelta

import pytest
from application.filters import CustomerFilter, UserFilter
from application.models import User
from application.tests.factories.customer import AddressFactory, CustomerFactory
from application.tests.factories.user import UserFactory
from django.utils import timezone
from freezegun import freeze_time


@pytest.mark.django_db
def test_user_filter_email_contains():
    """システムユーザ名を部分一致でフィルターできる事を確認する"""

    user = UserFactory(username="テストフィルターユーザ")
    user_filter = UserFilter({"email__contains": "テストフィルター"})
    assert user_filter.qs.count() == 1
    assert user_filter.qs[0] == user


@pytest.mark.django_db
def test_user_filter_email_contains():
    """メールアドレスを部分一致でフィルターできる事を確認する"""

    user = UserFactory(email="test_filter@test.com")
    user_filter = UserFilter({"email__contains": "test_filter"})
    assert user_filter.qs.count() == 1
    assert user_filter.qs[0] == user


@pytest.mark.django_db
def test_user_filter_role_in():
    """ロールを複数フィルターできる事を確認する"""

    User.objects.all().update(role=User.Role.PART_TIME)
    management_user = UserFactory(role=User.Role.MANAGEMENT)
    general_user = UserFactory(role=User.Role.MANAGEMENT)
    user_filter = UserFilter(
        {"role__in": f"{User.Role.MANAGEMENT},{User.Role.GENERAL}"}
    )
    assert user_filter.qs.count() == 2
    assert user_filter.qs[0] == management_user
    assert user_filter.qs[1] == general_user


@pytest.mark.django_db
def test_inquiry_application_date_range_filter():
    """作成日をフィルターできることを確認する"""

    today = timezone.now()
    with freeze_time(today):
        first_user = UserFactory()
        second_user = UserFactory()
        inquiry_filter = UserFilter({"created_at_after": today})
        assert inquiry_filter.qs.count() == 2
        assert inquiry_filter.qs[0] == first_user
        assert inquiry_filter.qs[1] == second_user


@pytest.mark.django_db
def test_customer_name_kana_filter_contains():
    """氏名・カナ氏名でフィルターできる事を確認する"""

    customer = CustomerFactory(
        name="降田絞込",
        kana="フィルタシボリコミ",
    )
    customer_filter = CustomerFilter({"name": "降田絞込"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer
    customer_filter = CustomerFilter({"name": "フィルタシボリコミ"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer


@pytest.mark.django_db
def test_customer_address_filter_contains():
    """住所でフィルターできる事を確認する"""

    address = AddressFactory()
    customer = CustomerFactory(address=address)
    customer_filter = CustomerFilter({"address": f"{address.prefecture}"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer
    customer_filter = CustomerFilter({"address": f"{address.municipalities}"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer
    customer_filter = CustomerFilter({"address": f"{address.house_no}"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer
    customer_filter = CustomerFilter({"address": f"{address.other}"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer


@pytest.mark.django_db
def test_customer_birthday_filter_exact():
    """誕生日を完全一致でフィルターできる事を確認する"""

    customer = CustomerFactory(birthday="1955-01-01")
    customer_filter = CustomerFilter({"birthday": "1955-01-01"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer


@pytest.mark.django_db
def test_customer_filter_email_contains():
    """メールアドレスを部分一致でフィルターできる事を確認する"""

    customer = CustomerFactory(email="test_filter@test.com")
    customer_filter = CustomerFilter({"email__contains": "test_filter"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer


@pytest.mark.django_db
def test_customer_phone_no_filter_starts_with():
    """電話番号を前方一致でフィルターできる事を確認する"""

    customer = CustomerFactory(phone_no="0120201810")
    customer_filter = CustomerFilter({"phone_no__startswith": "01202018"})
    assert customer_filter.qs.count() == 1
    assert customer_filter.qs[0] == customer

概括

虽然您可以实际发送请求进行测试,但使用django-filter编写代码会更简洁、更直观,因此我建议这样做。

bannerAds