【问题标题】:Prevent creation of related object while creating one using factory boy. To prevent database access in PyTest防止在使用工厂男孩创建相关对象时创建相关对象。防止 PyTest 中的数据库访问
【发布时间】:2021-09-06 23:43:01
【问题描述】:

我有两个模型

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 实例已创建并保存在数据库中,因此存在问题。但是如何防止这种情况,因为我不想在单元测试中进行数据库访问,也不能在没有单元测试的情况下离开序列化程序。

【问题讨论】:

    标签: django unit-testing django-rest-framework pytest


    【解决方案1】:

    终于找到答案了。我花了 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()
    

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 感谢 Dean,但是如果我想测试输出序列化程序的生成,如果它被破坏(由于模型的变化)我认为它很好进行单元测试。任何其他方式来做到这一点。 (我可以将此测试移至整数,但仍会在单元测试中寻找任何可能性)。
      • 我认为在这里使用单元测试会做很多工作,因为您需要覆盖很多 Django Rest Framework 的方法并创建很多我认为会使其不可读的模拟对象在我看来。如果您需要测试,我会在这里使用集成测试。
      • 如果您有一些自定义验证逻辑要在序列化程序上进行测试,那么使用 Python 的模拟库进行单元测试会很简单。
      猜你喜欢
      • 1970-01-01
      • 2018-03-16
      • 2014-06-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多