0

我们有一个包含大约 10,000 个 Django+Nose 单元测试的大型 Django 项目。我们很少在代码库中使用事务。可能我们 99% 的代码库不使用事务。该项目在 Django 1.5.8 和 Django Nose 1.4.1 上。(是的,我知道这已经很老了。我们目前正在进行一个更新到 Django 1.6 的项目 18 个月,但它还没有完成。所以,如果我的问题的解决方案是“升级 Django”,我我需要一种方法来修补它,因为这个问题现在正在发生,而且我们还需要几个月的时间才能完成 Django 的升级。)

我们今天遇到了一个以前从未见过的新错误。我们添加了一个新的数据库(和必要DATABASES['geo']的设置),其中包含一个应用程序不会更新的大型静态数据集。它是一个只读数据库,恰好存在于 MySQL 中。与我们所有其他数据库一样,Django Nose 在每次测试运行的开始(结束)时开始创建新数据库的测试副本(并破坏所述测试数据库)。这导致了许多问题,包括磁盘空间问题和浪费时间的问题,但测试确实运行并通过了。

为了解决这个问题,我们添加'TEST_MIRROR': 'geo'DATABASES['geo']设置。这就是这个头痛开始的地方。只是这种变化导致我们测试用例的一小部分随机部分在每次测试运行中失败:

<nose.suite.ContextSuite context=TestFacebookApiVersion>:setup
<nose.suite.ContextSuite context=RegisterPageTests>:setup
<nose.suite.ContextSuite context=CommonCeleryTasks>:setup
<nose.suite.ContextSuite context=CommonCeleryTestTasks>:setup
<nose.suite.ContextSuite context=S3PublishTestCase>:setup
<nose.suite.ContextSuite context=TestCEP>:setup
<nose.suite.ContextSuite context=AdbInvitesJsonTests>:setup

每个测试用例的错误和堆栈跟踪都是相同的:

Transaction managed block ended with pending COMMIT/ROLLBACK
Traceback (most recent call last):
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/nose/suite.py", line 209, in run
    self.setUp()
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/nose/suite.py", line 292, in setUp
    self.setupContext(ancestor)
  File "/var/lib/jenkins/workspace/my_workspace /my_project/lib/python2.7/site-packages/nose/suite.py", line 315, in setupContext
    try_run(context, names)
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/nose/util.py", line 471, in try_run
    return func()
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/django_nose/testcases.py", line 43, in setUpClass
    if not test.testcases.connections_support_transactions():
  File "/var/lib/jenkins/workspace/my_workspace /my_project/lib/python2.7/site-packages/django/test/testcases.py", line 827, in connections_support_transactions
    for conn in connections.all())
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/django/test/testcases.py", line 827, in <genexpr>
    for conn in connections.all())
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/django/utils/functional.py", line 45, in __get__
    res = instance.__dict__[self.func.__name__] = self.func(instance)
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/django/db/backends/__init__.py", line 455, in supports_transactions
    self.connection.leave_transaction_management()
  File "/var/lib/jenkins/workspace/my_workspace/my_project/lib/python2.7/site-packages/django/db/backends/__init__.py", line 138, in leave_transaction_management
    "Transaction managed block ended with pending COMMIT/ROLLBACK")

而且,更糟糕的是,少数失败的测试用例每次都不同。这是我第二次运行它时的失败:

<nose.suite.ContextSuite context=BaseTemplateContainerTests>:setup
<nose.suite.ContextSuite context=MessageServiceNotifierTests>:setup
<nose.suite.ContextSuite context=TestFormatShortAddress>:setup
<nose.suite.ContextSuite context=RevisionableTestCase>:setup
<nose.suite.ContextSuite context=TestSoaHelpers>:setup
<nose.suite.ContextSuite context=TestDateUtils>:setup

等等。

正如您从堆栈跟踪中看到的那样,执行甚至没有进入我们的源代码。在我们的测试用例开始执行之前,它在 Django Nose 源代码中失败了。而且,这只是我们测试的一小部分。其他 9,600 多个单元测试都以优异的成绩通过。

我不知道该怎么办。我不是故意创建任何事务,添加'TEST_MIRROR': 'geo'DATABASES['geo']配置中会导致这个问题对我来说没有意义,但确实如此。

我怎样才能解决这个问题?

4

1 回答 1

1

好吧,我花了很多时间调试,但我发现了问题......

我们的 10,000 个测试需要很长时间才能运行,除非我们在并行进程中运行它们。因此,我们使用了一个工具,将鼻子测试分成 20 个并行进程并分组运行测试。(这仍然需要大约 20 分钟才能完成,但比将近两个小时要好。)

我们使用FastFixtureTestCase,它扩展了TransactionTestCase. 在每个测试用例开始时,FastFixtureTestCase调用django.test.testcases: connections_support_transactions(). 该函数遍历所有DATABASES连接并调用supports_transactions每个连接。我的错误是假设这supports_transactions应该是一个本质上安全的操作。它不是。

supports_transactions执行以下操作:

  1. 创建一个新表
  2. 提交
  3. 向该表中插入一个值
  4. 回滚
  5. 选择该表中的行数
  6. 放下桌子
  7. 提交
  8. 返回True表中的行数是否为 0(表示回滚成功,因此必须支持事务)。

这是不安全的。这是非常危险的。没有两个进程或服务器可以同时针对同一个数据库运行此操作。如果两个或多个进程同时运行此函数,充其量,一个会返回True,另一个(或多个)会引发异常。在最坏的情况下,所有人都会引发异常。

在我的例子中,因为我们有这么多的测试用例,大部分时间进程都在避免connections_support_transactions同时执行,但是当他们这样做时,它会导致少量的随机失败,每次都不同。

正如@kmmbvnr指出的那样,一种可能的解决方案是使用SimpleTestCase而不是。但是,这对我们来说不是一个选择,因为我们的整个测试基础设施取决于对我们其余数据库的影响。因此,相反,我使用以下代码行仅针对静态共享数据库进行了覆盖,并且错误消失了:FastFixtureTestCaseFastFixtureTestCasesupports_transactions

connections['geo'].features.supports_transactions = True
于 2016-03-09T15:04:17.277 回答