Django测试:Postgres上的setUpTestData抛出:“重复的键值违反了唯一约束”

时间:2015-06-11 09:28:06

标签: django postgresql django-testing

我在单元测试中遇到了数据库问题。我认为这与我使用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.

我更改了所有主键,似乎运行正常。它似乎不影响任何测试。

然而,我担心我做错了什么。当它第一次发生时,我在我的应用程序上反转了大约一个小时的工作量(没有那么多的菜鸟代码),这纠正了问题。

然后,当我重新写入更改时,同样的问题又出现了。 TestCase粘贴在下面。在我添加sortrecord项目后,问题似乎发生了,但与上面的项目相对应。

我不想在我的测试中继续检查和更改主键和网址,所以如果有人发现我使用它的方式有问题,请帮助我。谢谢!

测试用例

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()的两个实例中将主键与不​​同的值匹配,果然,我得到了同样的错误。

在我向其中一个添加更多数据之前,这两个设置并排运行良好。现在,他们似乎需要不同的价值观。我想你只能使用重复数据这么长时间。

6 个答案:

答案 0 :(得分:3)

几个月来,我一直不时遇到这个问题。我相信我已经找到了根本原因和一些解决方案。

摘要 无论出于何种原因,似乎Django测试用例的基类都没有删除由我们创建的数据库记录(在运行using System.ComponentModel; using System.Runtime.InteropServices; using System.Windows.Forms; [DesignerCategory("Code")] class DateTimePickerYearMonth : DateTimePicker { [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); private const int DTM_FIRST = 0x1000; private const int DTM_GETMONTHCAL = DTM_FIRST + 8; private const int DTM_SETMCSTYLE = DTM_FIRST + 11; private const int DTM_GETMCSTYLE = DTM_FIRST + 12; private const int MCM_FIRST = 0x1000; private const int MCM_GETCURRENTVIEW = MCM_FIRST + 22; private const int MCM_SETCURRENTVIEW = MCM_FIRST + 32; private bool m_ShowToday = false; public enum MonCalView : int { MCMV_MONTH = 0, MCMV_YEAR = 1, MCMV_DECADE = 2, MCMV_CENTURY = 3 } public enum MonCalStyles : int { MCS_DAYSTATE = 0x0001, MCS_MULTISELECT = 0x0002, MCS_WEEKNUMBERS = 0x0004, MCS_NOTODAYCIRCLE = 0x0008, MCS_NOTODAY = 0x0010, MCS_NOTRAILINGDATES = 0x0040, MCS_SHORTDAYSOFWEEK = 0x0080, MCS_NOSELCHANGEONNAV = 0x0100 } public DateTimePickerYearMonth() { this.CustomFormat = "MM-yyyy"; this.Format = DateTimePickerFormat.Custom; this.Value = DateTime.Now; } [ Browsable(true), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(false), Category("Appearance"), Description("Shows or hides \"Today\" date at the bottom of the Calendar Control") ] public bool ShowToday { get => m_ShowToday; set { if (value != m_ShowToday) { m_ShowToday = value; ShowMonCalToday(m_ShowToday); } } } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); ShowMonCalToday(m_ShowToday); } protected override void OnDropDown(EventArgs e) { var hWnd = SendMessage(this.Handle, DTM_GETMONTHCAL, 0, 0); if (hWnd != IntPtr.Zero) { SendMessage(hWnd, MCM_SETCURRENTVIEW, 0, (int)MonCalView.MCMV_YEAR); } base.OnDropDown(e); } private void ShowMonCalToday(bool show) { int styles = SendMessage(this.Handle, DTM_GETMCSTYLE, 0, 0).ToInt32(); styles = show ? styles &~(int)MonCalStyles.MCS_NOTODAY : styles | (int)MonCalStyles.MCS_NOTODAY; SendMessage(this.Handle, DTM_SETMCSTYLE, 0, styles); } } 之前将其命名为TestCase1)。在TestCase2中,当尝试使用与TestCase2相同的ID在数据库中创建记录时,数据库会引发TestCase1异常,因为这些ID已存在于数据库中。甚至说出神奇的单词“ please”也不会帮助解决数据库重复的键错误。

好消息是,有多种方法可以解决此问题!这是一对...

解决方案1 ​​

请确保您是否覆盖要调用的DuplicateKey类方法tearDownClass。如果您覆盖super().tearDownClass()而未调用其超级,则它将永远不会调用tearDownClass()TransactionTestCase._post_teardown()。引用TransactionTestCase._post_teardown()`中的文档字符串:

TransactionTestCase._fixture_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(),则在测试用例之间不会重置数据库,您将得到可怕的重复密钥异常。

解决方案2

覆盖super()并设置类变量TransactionTestCase,如下所示:

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时,Django测试运行器将回滚测试用例之间插入数据库的所有事务。还有batta bing,batta bang ...不再有重复的键错误!

结论

可能有更多的方法可以为OP的问题实施解决方案,但是这两种方法应该可以很好地工作。为了清楚起见,以及更深入地了解底层Django测试用例基类,绝对希望其他人添加更多解决方案。 ew,说最后一行真快三遍,您可以赢一匹小马!

答案 1 :(得分:1)

在没有任何重复数据的情况下,我继续遇到此错误,但是我能够通过初始化对象并调用result方法来解决此问题,而不是通过save()创建对象

换句话说,我做到了:

Model.objects.create()

代替此:

@classmethod
def setUpTestData(cls):
    cls.person = Person(first_name="Jane", last_name="Doe")
    cls.person.save()

答案 2 :(得分:0)

您提供的日志指出DETAIL: Key (product_name)=(Almonds) already exists。你在数据库中验证了吗?

为防止将来出现此类错误,您应该在test_

之前为所有测试数据字符串添加前缀

答案 3 :(得分:0)

我发现了这个问题,如问题的底部所述。

据我所知,数据库不喜欢我在两个不同测试的setUpTestData()方法中使用重复数据。在第二次测试中更改主键值可以解决问题。

答案 4 :(得分:0)

我认为这里的问题是你的TestCase中有一个tearDownClass方法而没有调用super方法。 这样,django TestCase就丢失了setUpTestData背后的事务功能,因此在TestCase完成后它不会清理你的测试数据库。

在这里查看django文档中的警告: https://docs.djangoproject.com/en/1.10/topics/testing/tools/#django.test.SimpleTestCase.allow_database_queries

答案 5 :(得分:0)

我遇到了类似的问题,这是由显式地向测试用例提供主键值引起的。

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;
                """
            )