Django 测试:Postgres 上的 setUpTestData 抛出:"Duplicate key value violates unique constraint"
Django Tests: setUpTestData on Postgres throws: "Duplicate key value violates unique constraint"
我 运行 在我的单元测试中遇到数据库问题。我认为这与我使用 TestCase 和 setUpData 的方式有关。
当我尝试使用某些值设置我的测试数据时,测试抛出以下错误:
django.db.utils.IntegrityError: duplicate key value violates unique constraint
...
psycopg2.IntegrityError: duplicate key value violates unique constraint "InventoryLogs_productgroup_product_name_48ec6f8d_uniq"
DETAIL: Key (product_name)=(Almonds) already exists.
我更改了所有主键,看起来 运行 没问题。它似乎不会影响任何测试。
但是,我担心我做错了什么。当它第一次发生时,我在我的应用程序上撤消了大约一个小时的工作(对于新手来说不是那么多代码),这纠正了问题。
然后当我写回更改时,同样的问题又出现了。测试用例粘贴在下面。这个问题似乎是在我添加 sortrecord 项目后出现的,但与上面的项目相对应。
我不想在我的测试中不断检查和更改主键和 url,所以如果有人发现我使用它的方式有问题,请帮助我。谢谢!
测试用例
class DetailsPageTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.product1 = ProductGroup.objects.create(
product_name="Almonds"
)
cls.variety1 = Variety.objects.create(
product_group = cls.product1,
variety_name = "non pareil",
husked = False,
finished = False,
)
cls.supplier1 = Supplier.objects.create(
company_name = "Acme",
company_location = "Acme Acres",
contact_info = "Call me!"
)
cls.shipment1 = Purchase.objects.create(
tag=9,
shipment_id=9999,
supplier_id = cls.supplier1,
purchase_date='2015-01-09',
purchase_price=9.99,
product_name=cls.variety1,
pieces=99,
kgs=999,
crackout_estimate=99.9
)
cls.shipment2 = Purchase.objects.create(
tag=8,
shipment_id=8888,
supplier_id=cls.supplier1,
purchase_date='2015-01-08',
purchase_price=8.88,
product_name=cls.variety1,
pieces=88,
kgs=888,
crackout_estimate=88.8
)
cls.shipment3 = Purchase.objects.create(
tag=7,
shipment_id=7777,
supplier_id=cls.supplier1,
purchase_date='2014-01-07',
purchase_price=7.77,
product_name=cls.variety1,
pieces=77,
kgs=777,
crackout_estimate=77.7
)
cls.sortrecord1 = SortingRecords.objects.create(
tag=cls.shipment1,
date="2015-02-05",
bags_sorted=20,
turnout=199,
)
cls.sortrecord2 = SortingRecords.objects.create(
tag=cls.shipment1,
date="2015-02-07",
bags_sorted=40,
turnout=399,
)
cls.sortrecord3 = SortingRecords.objects.create(
tag=cls.shipment1,
date='2015-02-09',
bags_sorted=30,
turnout=299,
)
型号
from datetime import datetime
from django.db import models
from django.db.models import Q
class ProductGroup(models.Model):
product_name = models.CharField(max_length=140, primary_key=True)
def __str__(self):
return self.product_name
class Meta:
verbose_name = "Product"
class Supplier(models.Model):
company_name = models.CharField(max_length=45)
company_location = models.CharField(max_length=45)
contact_info = models.CharField(max_length=256)
class Meta:
ordering = ["company_name"]
def __str__(self):
return self.company_name
class Variety(models.Model):
product_group = models.ForeignKey(ProductGroup)
variety_name = models.CharField(max_length=140)
husked = models.BooleanField()
finished = models.BooleanField()
description = models.CharField(max_length=500, blank=True)
class Meta:
ordering = ["product_group_id"]
verbose_name_plural = "Varieties"
def __str__(self):
return self.variety_name
class PurchaseYears(models.Manager):
def purchase_years_list(self):
unique_years = Purchase.objects.dates('purchase_date', 'year')
results_list = []
for p in unique_years:
results_list.append(p.year)
return results_list
class Purchase(models.Model):
tag = models.IntegerField(primary_key=True)
product_name = models.ForeignKey(Variety, related_name='purchases')
shipment_id = models.CharField(max_length=24)
supplier_id = models.ForeignKey(Supplier)
purchase_date = models.DateField()
estimated_delivery = models.DateField(null=True, blank=True)
purchase_price = models.DecimalField(max_digits=6, decimal_places=3)
pieces = models.IntegerField()
kgs = models.IntegerField()
crackout_estimate = models.DecimalField(max_digits=6,decimal_places=3, null=True)
crackout_actual = models.DecimalField(max_digits=6,decimal_places=3, null=True)
objects = models.Manager()
purchase_years = PurchaseYears()
# Keep manager as "objects" in case admin, etc. needs it. Filter can be called like so:
# Purchase.objects.purchase_years_list()
# Managers in docs: https://docs.djangoproject.com/en/1.8/intro/tutorial01/
class Meta:
ordering = ["purchase_date"]
def __str__(self):
return self.shipment_id
def _weight_conversion(self):
return round(self.kgs * 2.20462)
lbs = property(_weight_conversion)
class SortingModelsBagsCalulator(models.Manager):
def total_sorted(self, record_date, current_set):
sorted = [SortingRecords['bags_sorted'] for SortingRecords in current_set if
SortingRecords['date'] <= record_date]
return sum(sorted)
class SortingRecords(models.Model):
tag = models.ForeignKey(Purchase, related_name='sorting_record')
date = models.DateField()
bags_sorted = models.IntegerField()
turnout = models.IntegerField()
objects = models.Manager()
def __str__(self):
return "%s [%s]" % (self.date, self.tag.tag)
class Meta:
ordering = ["date"]
verbose_name_plural = "Sorting Records"
def _calculate_kgs_sorted(self):
kg_per_bag = self.tag.kgs / self.tag.pieces
kgs_sorted = kg_per_bag * self.bags_sorted
return (round(kgs_sorted, 2))
kgs_sorted = property(_calculate_kgs_sorted)
def _byproduct(self):
waste = self.kgs_sorted - self.turnout
return (round(waste, 2))
byproduct = property(_byproduct)
def _bags_remaining(self):
current_set = SortingRecords.objects.values().filter(~Q(id=self.id), tag=self.tag)
sorted = [SortingRecords['bags_sorted'] for SortingRecords in current_set if
SortingRecords['date'] <= self.date]
remaining = self.tag.pieces - sum(sorted) - self.bags_sorted
return remaining
bags_remaining = property(_bags_remaining)
编辑
它也像这样用整数失败。
django.db.utils.IntegrityError: duplicate key value violates unique constraint "InventoryLogs_purchase_pkey"
DETAIL: Key (tag)=(9) already exists.
UDPATE
所以我应该早点提到这个,但我完全忘记了。我有两个使用相同数据的单元测试文件。只是为了好玩,我将 setUpTestData()
的两个实例中的主键匹配到不同的值,果然,我得到了同样的错误。
在我向其中一个添加更多数据之前,这两个设置并排运行良好。现在,他们似乎需要不同的价值观。我想你只能在这么长时间内使用重复数据才能逃脱。
您提供的日志显示 DETAIL: Key (product_name)=(Almonds) already exists
。你在你的数据库中验证了吗?
为防止将来出现此类错误,您应该在所有测试数据字符串前加上 test_
我发现了问题,如问题底部所述。
据我所知,数据库不喜欢我在两个不同测试的 setUpTestData()
方法中使用重复数据。在第二个测试中更改主键值更正了问题。
我认为这里的问题是您的 TestCase 中有一个 tearDownClass 方法,但没有调用 super 方法。
通过这种方式,django TestCase 丢失了 setUpTestData 背后的事务功能,因此它不会在 TestCase 完成后清理您的测试数据库。
在此处检查 django 文档中的警告:
https://docs.djangoproject.com/en/1.10/topics/testing/tools/#django.test.SimpleTestCase.allow_database_queries
我在没有任何重复数据的情况下继续收到此错误,但我能够通过初始化对象并调用 save()
方法而不是通过 Model.objects.create()
[= 创建对象来解决问题14=]
换句话说,我是这样做的:
@classmethod
def setUpTestData(cls):
cls.person = Person(first_name="Jane", last_name="Doe")
cls.person.save()
而不是这个:
@classmethod
def setUpTestData(cls):
cls.person = Person.objects.create(first_name="Jane", last_name="Doe")
我 运行 几个月来偶尔会遇到这个问题。我相信我只是找出了根本原因和几个解决方案。
总结
无论出于何种原因,Django 测试用例库 classes 似乎都没有删除由我们在 运行 TestCase2
之前将其称为 TestCase1
创建的数据库记录。其中,在 TestCase2
中,当它尝试使用与 TestCase1
相同的 ID 在数据库中创建记录时,数据库会引发 DuplicateKey
异常,因为这些 ID 已经存在于数据库中。甚至说魔术词 "please" 也无助于解决数据库重复键错误。
好消息是,有多种方法可以解决这个问题!这里有一对...
解决方案 1
确保您是否覆盖了调用 super().tearDownClass()
的 class 方法 tearDownClass
。如果你重写 tearDownClass()
而没有调用它的 super,它将永远不会调用 TransactionTestCase._post_teardown()
或 TransactionTestCase._fixture_teardown()
。引用 TransactionTestCase._post_teardown()`:
中的文档字符串
def _post_teardown(self):
"""
Perform post-test things:
* Flush the contents of the database to leave a clean slate. If the
class has an 'available_apps' attribute, don't fire post_migrate.
* Force-close the connection so the next test gets a clean cursor.
"""
如果 TestCase.tearDownClass()
不是通过 super()
调用的,那么数据库不会在测试用例之间重置,您将得到可怕的重复键异常。
解决方案 2
覆盖 TransactionTestCase
并设置 class 变量 serialized_rollback = True
,如下所示:
class MyTestCase(TransactionTestCase):
fixtures = ['test-data.json']
serialized_rollback = True
def test_name_goes_here(self):
pass
引用来源:
class TransactionTestCase(SimpleTestCase):
...
# If transactions aren't available, Django will serialize the database
# contents into a fixture during setup and flush and reload them
# during teardown (as flush does not restore data from migrations).
# This can be slow; this flag allows enabling on a per-case basis.
serialized_rollback = False
当 serialized_rollback
设置为 True
时,Django 测试运行程序会回滚在测试用例之间插入数据库的任何事务。还有 batta bing,batta bang...不再有重复键错误!
结论
可能有更多方法可以解决 OP 的问题,但这两种方法应该可以很好地工作。为了清楚起见和更深入地了解底层 Django 测试用例库 classes,肯定希望其他人添加更多解决方案。呸,快说最后一句三遍,你就能赢一匹小马!
我遇到了类似的问题,该问题是由显式向测试用例提供主键值引起的。
如 the Django documentation 中所述,手动为自动递增字段分配值不会更新字段的序列,这可能会在以后引起冲突。
我已经通过手动更改顺序解决了它:
from django.db import connection
class MyTestCase(TestCase):
@classmethod
def setUpTestData(cls):
Model.objects.create(id=1)
with connection.cursor() as c:
c.execute(
"""
ALTER SEQUENCE "app_model_id_seq" RESTART WITH 2;
"""
)
我 运行 在我的单元测试中遇到数据库问题。我认为这与我使用 TestCase 和 setUpData 的方式有关。
当我尝试使用某些值设置我的测试数据时,测试抛出以下错误:
django.db.utils.IntegrityError: duplicate key value violates unique constraint
...
psycopg2.IntegrityError: duplicate key value violates unique constraint "InventoryLogs_productgroup_product_name_48ec6f8d_uniq"
DETAIL: Key (product_name)=(Almonds) already exists.
我更改了所有主键,看起来 运行 没问题。它似乎不会影响任何测试。
但是,我担心我做错了什么。当它第一次发生时,我在我的应用程序上撤消了大约一个小时的工作(对于新手来说不是那么多代码),这纠正了问题。
然后当我写回更改时,同样的问题又出现了。测试用例粘贴在下面。这个问题似乎是在我添加 sortrecord 项目后出现的,但与上面的项目相对应。
我不想在我的测试中不断检查和更改主键和 url,所以如果有人发现我使用它的方式有问题,请帮助我。谢谢!
测试用例
class DetailsPageTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.product1 = ProductGroup.objects.create(
product_name="Almonds"
)
cls.variety1 = Variety.objects.create(
product_group = cls.product1,
variety_name = "non pareil",
husked = False,
finished = False,
)
cls.supplier1 = Supplier.objects.create(
company_name = "Acme",
company_location = "Acme Acres",
contact_info = "Call me!"
)
cls.shipment1 = Purchase.objects.create(
tag=9,
shipment_id=9999,
supplier_id = cls.supplier1,
purchase_date='2015-01-09',
purchase_price=9.99,
product_name=cls.variety1,
pieces=99,
kgs=999,
crackout_estimate=99.9
)
cls.shipment2 = Purchase.objects.create(
tag=8,
shipment_id=8888,
supplier_id=cls.supplier1,
purchase_date='2015-01-08',
purchase_price=8.88,
product_name=cls.variety1,
pieces=88,
kgs=888,
crackout_estimate=88.8
)
cls.shipment3 = Purchase.objects.create(
tag=7,
shipment_id=7777,
supplier_id=cls.supplier1,
purchase_date='2014-01-07',
purchase_price=7.77,
product_name=cls.variety1,
pieces=77,
kgs=777,
crackout_estimate=77.7
)
cls.sortrecord1 = SortingRecords.objects.create(
tag=cls.shipment1,
date="2015-02-05",
bags_sorted=20,
turnout=199,
)
cls.sortrecord2 = SortingRecords.objects.create(
tag=cls.shipment1,
date="2015-02-07",
bags_sorted=40,
turnout=399,
)
cls.sortrecord3 = SortingRecords.objects.create(
tag=cls.shipment1,
date='2015-02-09',
bags_sorted=30,
turnout=299,
)
型号
from datetime import datetime
from django.db import models
from django.db.models import Q
class ProductGroup(models.Model):
product_name = models.CharField(max_length=140, primary_key=True)
def __str__(self):
return self.product_name
class Meta:
verbose_name = "Product"
class Supplier(models.Model):
company_name = models.CharField(max_length=45)
company_location = models.CharField(max_length=45)
contact_info = models.CharField(max_length=256)
class Meta:
ordering = ["company_name"]
def __str__(self):
return self.company_name
class Variety(models.Model):
product_group = models.ForeignKey(ProductGroup)
variety_name = models.CharField(max_length=140)
husked = models.BooleanField()
finished = models.BooleanField()
description = models.CharField(max_length=500, blank=True)
class Meta:
ordering = ["product_group_id"]
verbose_name_plural = "Varieties"
def __str__(self):
return self.variety_name
class PurchaseYears(models.Manager):
def purchase_years_list(self):
unique_years = Purchase.objects.dates('purchase_date', 'year')
results_list = []
for p in unique_years:
results_list.append(p.year)
return results_list
class Purchase(models.Model):
tag = models.IntegerField(primary_key=True)
product_name = models.ForeignKey(Variety, related_name='purchases')
shipment_id = models.CharField(max_length=24)
supplier_id = models.ForeignKey(Supplier)
purchase_date = models.DateField()
estimated_delivery = models.DateField(null=True, blank=True)
purchase_price = models.DecimalField(max_digits=6, decimal_places=3)
pieces = models.IntegerField()
kgs = models.IntegerField()
crackout_estimate = models.DecimalField(max_digits=6,decimal_places=3, null=True)
crackout_actual = models.DecimalField(max_digits=6,decimal_places=3, null=True)
objects = models.Manager()
purchase_years = PurchaseYears()
# Keep manager as "objects" in case admin, etc. needs it. Filter can be called like so:
# Purchase.objects.purchase_years_list()
# Managers in docs: https://docs.djangoproject.com/en/1.8/intro/tutorial01/
class Meta:
ordering = ["purchase_date"]
def __str__(self):
return self.shipment_id
def _weight_conversion(self):
return round(self.kgs * 2.20462)
lbs = property(_weight_conversion)
class SortingModelsBagsCalulator(models.Manager):
def total_sorted(self, record_date, current_set):
sorted = [SortingRecords['bags_sorted'] for SortingRecords in current_set if
SortingRecords['date'] <= record_date]
return sum(sorted)
class SortingRecords(models.Model):
tag = models.ForeignKey(Purchase, related_name='sorting_record')
date = models.DateField()
bags_sorted = models.IntegerField()
turnout = models.IntegerField()
objects = models.Manager()
def __str__(self):
return "%s [%s]" % (self.date, self.tag.tag)
class Meta:
ordering = ["date"]
verbose_name_plural = "Sorting Records"
def _calculate_kgs_sorted(self):
kg_per_bag = self.tag.kgs / self.tag.pieces
kgs_sorted = kg_per_bag * self.bags_sorted
return (round(kgs_sorted, 2))
kgs_sorted = property(_calculate_kgs_sorted)
def _byproduct(self):
waste = self.kgs_sorted - self.turnout
return (round(waste, 2))
byproduct = property(_byproduct)
def _bags_remaining(self):
current_set = SortingRecords.objects.values().filter(~Q(id=self.id), tag=self.tag)
sorted = [SortingRecords['bags_sorted'] for SortingRecords in current_set if
SortingRecords['date'] <= self.date]
remaining = self.tag.pieces - sum(sorted) - self.bags_sorted
return remaining
bags_remaining = property(_bags_remaining)
编辑
它也像这样用整数失败。
django.db.utils.IntegrityError: duplicate key value violates unique constraint "InventoryLogs_purchase_pkey"
DETAIL: Key (tag)=(9) already exists.
UDPATE
所以我应该早点提到这个,但我完全忘记了。我有两个使用相同数据的单元测试文件。只是为了好玩,我将 setUpTestData()
的两个实例中的主键匹配到不同的值,果然,我得到了同样的错误。
在我向其中一个添加更多数据之前,这两个设置并排运行良好。现在,他们似乎需要不同的价值观。我想你只能在这么长时间内使用重复数据才能逃脱。
您提供的日志显示 DETAIL: Key (product_name)=(Almonds) already exists
。你在你的数据库中验证了吗?
为防止将来出现此类错误,您应该在所有测试数据字符串前加上 test_
我发现了问题,如问题底部所述。
据我所知,数据库不喜欢我在两个不同测试的 setUpTestData()
方法中使用重复数据。在第二个测试中更改主键值更正了问题。
我认为这里的问题是您的 TestCase 中有一个 tearDownClass 方法,但没有调用 super 方法。 通过这种方式,django TestCase 丢失了 setUpTestData 背后的事务功能,因此它不会在 TestCase 完成后清理您的测试数据库。
在此处检查 django 文档中的警告: https://docs.djangoproject.com/en/1.10/topics/testing/tools/#django.test.SimpleTestCase.allow_database_queries
我在没有任何重复数据的情况下继续收到此错误,但我能够通过初始化对象并调用 save()
方法而不是通过 Model.objects.create()
[= 创建对象来解决问题14=]
换句话说,我是这样做的:
@classmethod
def setUpTestData(cls):
cls.person = Person(first_name="Jane", last_name="Doe")
cls.person.save()
而不是这个:
@classmethod
def setUpTestData(cls):
cls.person = Person.objects.create(first_name="Jane", last_name="Doe")
我 运行 几个月来偶尔会遇到这个问题。我相信我只是找出了根本原因和几个解决方案。
总结
无论出于何种原因,Django 测试用例库 classes 似乎都没有删除由我们在 运行 TestCase2
之前将其称为 TestCase1
创建的数据库记录。其中,在 TestCase2
中,当它尝试使用与 TestCase1
相同的 ID 在数据库中创建记录时,数据库会引发 DuplicateKey
异常,因为这些 ID 已经存在于数据库中。甚至说魔术词 "please" 也无助于解决数据库重复键错误。
好消息是,有多种方法可以解决这个问题!这里有一对...
解决方案 1
确保您是否覆盖了调用 super().tearDownClass()
的 class 方法 tearDownClass
。如果你重写 tearDownClass()
而没有调用它的 super,它将永远不会调用 TransactionTestCase._post_teardown()
或 TransactionTestCase._fixture_teardown()
。引用 TransactionTestCase._post_teardown()`:
def _post_teardown(self):
"""
Perform post-test things:
* Flush the contents of the database to leave a clean slate. If the
class has an 'available_apps' attribute, don't fire post_migrate.
* Force-close the connection so the next test gets a clean cursor.
"""
如果 TestCase.tearDownClass()
不是通过 super()
调用的,那么数据库不会在测试用例之间重置,您将得到可怕的重复键异常。
解决方案 2
覆盖 TransactionTestCase
并设置 class 变量 serialized_rollback = True
,如下所示:
class MyTestCase(TransactionTestCase):
fixtures = ['test-data.json']
serialized_rollback = True
def test_name_goes_here(self):
pass
引用来源:
class TransactionTestCase(SimpleTestCase):
...
# If transactions aren't available, Django will serialize the database
# contents into a fixture during setup and flush and reload them
# during teardown (as flush does not restore data from migrations).
# This can be slow; this flag allows enabling on a per-case basis.
serialized_rollback = False
当 serialized_rollback
设置为 True
时,Django 测试运行程序会回滚在测试用例之间插入数据库的任何事务。还有 batta bing,batta bang...不再有重复键错误!
结论
可能有更多方法可以解决 OP 的问题,但这两种方法应该可以很好地工作。为了清楚起见和更深入地了解底层 Django 测试用例库 classes,肯定希望其他人添加更多解决方案。呸,快说最后一句三遍,你就能赢一匹小马!
我遇到了类似的问题,该问题是由显式向测试用例提供主键值引起的。
如 the Django documentation 中所述,手动为自动递增字段分配值不会更新字段的序列,这可能会在以后引起冲突。
我已经通过手动更改顺序解决了它:
from django.db import connection
class MyTestCase(TestCase):
@classmethod
def setUpTestData(cls):
Model.objects.create(id=1)
with connection.cursor() as c:
c.execute(
"""
ALTER SEQUENCE "app_model_id_seq" RESTART WITH 2;
"""
)