使用工厂男孩创建相关对象时防止创建相关对象。防止 PyTest 中的数据库访问

Prevent creation of related object while creating one using factory boy. To prevent database access in PyTest

我有两个模型

import uuid

from django.db import models


class Currency(models.Model):
    """Currency model"""
    name = models.CharField(max_length=120, null=False,
                            blank=False, unique=True)
    code = models.CharField(max_length=3, null=False, blank=False, unique=True)
    symbol = models.CharField(max_length=5, null=False,
                              blank=False, default='$')

    def __str__(self) -> str:
        return self.code


class Transaction(models.Model):
    uid = models.UUIDField(default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=50, null=False, blank=False)
    email = models.EmailField(max_length=50, null=False, blank=False)
    creation_date = models.DateTimeField(auto_now_add=True)
    currency = models.ForeignKey(
        Currency, null=False, blank=False, on_delete=models.PROTECT)
    payment_intent_id = models.CharField(
        max_length=100, null=True, blank=False, default=None)
    message = models.TextField(null=True, blank=True)

    def __str__(self) -> str:
        return f"{self.name} - {self.id} : {self.currency}"

    @property
    def link(self):
        """
            Link to a payment form for the transaction
        """
        return f'http://127.0.0.1:8000/payment/{str(self.id)}'

和三个序列化程序

from django.conf import settings
from django.core.validators import (MaxLengthValidator,
                                    ProhibitNullCharactersValidator)
from rest_framework import serializers

from apps.Payment.models import Currency, Transaction


class CurrencySerializer(serializers.ModelSerializer):

    class Meta:
        model = Currency
        fields = ['name', 'code', 'symbol']
        if settings.DEBUG == True:
            extra_kwargs = {
                'name': {
                    'validators': [MaxLengthValidator, ProhibitNullCharactersValidator]
                },
                'code': {
                    'validators': [MaxLengthValidator, ProhibitNullCharactersValidator]
                }
            }


class UnfilledTransactionSerializer(serializers.ModelSerializer):
    currency = serializers.SlugRelatedField(
        slug_field='code',
        queryset=Currency.objects.all(),
    )

    class Meta:
        model = Transaction
        fields = (
            'name',
            'currency',
            'email',
            'message'
        )


class FilledTransactionSerializer(serializers.ModelSerializer):
    currency = serializers.StringRelatedField(read_only=True)
    link = serializers.ReadOnlyField()

    class Meta:
        model = Transaction
        fields = '__all__'
        extra_kwargs = {
            """Non editable fields"""
            'id': {'read_only': True},
            'creation_date': {'read_only': True},
            'payment_intent_id': {'read_only': True},
        }

还有两个视图

from rest_framework.viewsets import ModelViewSet

from apps.Payment.models import Currency, Transaction
from apps.Payment.serializers import (CurrencySerializer,
                                      FilledTransactionSerializer,
                                      UnfilledTransactionSerializer)


class CurrencyViewSet(ModelViewSet):
    queryset = Currency.objects.all()
    serializer_class = CurrencySerializer


class TransactionViewset(ModelViewSet):
    """Transaction Viewset"""

    queryset = Transaction.objects.all()

    def get_serializer_class(self):
        if self.action == 'create':
            return UnfilledTransactionSerializer
        else:
            return FilledTransactionSerializer

也是一个信号

from django.db.models.signals import post_save
from django.dispatch import receiver

from apps.Payment.models import Transaction
from apps.Payment.utils import fill_transaction


@receiver(post_save, sender=Transaction)
def transaction_filler(sender, instance, created, *args, **kwargs):
    ''' fill 'payment_intent_id' field in a transacton before saving '''

    if created:
        fill_transaction(instance)

在下面的代码中,两个测试用例都通过了(因为我在测试时使用了数据库)

class TestUnfilledTransactionSerializer:
    def test_serialize_model(self):
        transaction = TransactionFactory.build()
        expected_serialized_data = {
            'name': transaction.name,
            'currency': transaction.currency.code,
            'email': transaction.email,
            'message': transaction.message,
        }

        serializer = UnfilledTransactionSerializer(transaction)
        assert serializer.data == expected_serialized_data

    @pytest.mark.django_db
    def test_serialized_data(self):
        c = CurrencyFactory()
        transaction = TransactionFactory.build(currency=c)

        valid_serialized_data = {
            'name': transaction.name,
            'currency': transaction.currency.code,
            'email': transaction.email,
            'message': transaction.message,
        }

        serializer = UnfilledTransactionSerializer(data=valid_serialized_data)
        
        assert serializer.is_valid(raise_exception=True)
        assert serializer.errors == {}

但是,一旦我从第二个测试用例中删除数据库访问权限,我就会收到此错误。我知道为什么会出现此错误。

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

这里是工厂的代码

import factory

from apps.Payment.models import Transaction, Currency
from faker import Faker
fake = Faker()


class CurrencyFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Currency

    # Since currency is declared as a parameter, it won't be passed to 
    # the model (it's automatically added to Meta.exclude.
    class Params:
        currency = factory.Faker("currency")  # (code, name)

    code = factory.LazyAttribute(lambda o: o.currency[0])
    name = factory.LazyAttribute(lambda o: o.currency[1])
    symbol = '$'


class TransactionFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Transaction
    
    # if we do not assign these attributes here then they will remain blank
    
    # currency is auto generated on creation of transaction
    currency = factory.SubFactory(CurrencyFactory)
    payment_intent_id = None
    email = factory.LazyAttribute(lambda _: fake.email())
    name = factory.LazyAttribute(lambda _: fake.name())

现在的问题

我知道 Currency 实例已创建并保存在数据库中,因此出现了问题。但是如何防止这种情况发生,因为我不想在单元测试中访问数据库,也不能在没有单元测试的情况下离开序列化程序。

可以使用 Python 的 mock 库编写单元测试。但是,我不推荐它。您似乎没有在这里测试任何自定义逻辑,而只是测试 Django Rest Framework 代码,它们有自己的测试。

终于找到答案了。我花了 2 天时间。

简答:不要让事务工厂创建依赖实例并将其保存到数据库,单独创建依赖实例并引用它们。但是不要忘记在调用依赖实例的地方模拟调用,因为它没有保存到数据库中,我们需要模拟它的调用。

代码

test_serializer.py

from rest_framework.relations import SlugRelatedField

class TestUnfilledTransactionSerializer:
    
    def test_serialized_data(self, mocker):
        # this do not save the instance in DB 
        currency = CurrencyFactory.build()
        transaction = CurrencylessTransactionFactory.build()
        transaction.currency = currency

        valid_serialized_data = {
            'name': transaction.name,
            'currency': transaction.currency.code,
            'email': transaction.email,
            'message': transaction.message,
        }

        # we do this to avoid searching DB for currency instance 
        # with respective currency code
        retrieve_currency = mocker.Mock(return_value=currency)
        SlugRelatedField.to_internal_value = retrieve_currency

        serializer = UnfilledTransactionSerializer(data=valid_serialized_data)
        
        assert serializer.is_valid(raise_exception=True)
        assert serializer.errors == {}

factory.py

import factory

from apps.Payment.models import Transaction, Currency
from faker import Faker
fake = Faker()

class CurrencylessTransactionFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Transaction
    
    email = factory.LazyAttribute(lambda _: fake.email())
    name = factory.LazyAttribute(lambda _: fake.name())
    message = fake.text()