使用工厂男孩创建相关对象时防止创建相关对象。防止 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()
我有两个模型
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()