从Django 1.0.4 升级到1.1.1,部分UnitTest不通过的原因。

先说结论:
Django 1.1.1 为了提高UnitTest的执行速度,新增了一个TransationTestCase,而原来的Django 1.0.4 中的TestCase(下面用TestCase104和TestCase111来标示不同版本的TestCase)变成了TransactionTestCase的子类。
注意,TestCase111的执行速度较快,而不是TransactionTestCase,原因下面解释。

如果你的测试在1.0.4没问题,在1.1.1失败,那么试试使用TransactionTestCase。

失败的例子:
1、用户登录(无论是测试用的client.login,还是直接通过登录页面post数据);
2、访问某个view,带有登录校验的(如@login_required);
此时也许会报告未登录。

原因概述:
TestCase111屏蔽了方法内部的事务处理,而在方法首尾包裹一层事务,用于回滚在测试方法执行过程中修改的数据。
当你登录用户时,提示登录成功,之后,数据库链接可能会断开,此时由于事务被屏蔽,无法在断开前commit,此时数据库自动调用了rollback,导致刚才登录的信息丢失(比如保存到数据库的session信息)。访问view时,自然无法找到对应的登录信息。

原因详述:
原来TestCase104的一个测试方法执行完毕之后,框架就会自动调用一次flush命令,具体的数据库命令就是
BEGIN;
TRUNCATE "app1_table1", "app2_table1",......;

SELECT setval('"app1_table1_id_seq"', 1, false);
.......
COMMIT; 

这里列举的是PostgreSQL的SQL。

那么一个测试方法的执行过程就是:
1、加载测试数据(如果有的话);
2、运行测试;
3、清除数据库。

而在TestCase111中,flush命令被屏蔽了,取而代之的是,使用了事务的回滚操作来做同样的事情。
那么测试方法的执行过程被简化为被简化为:
1、运行测试;
2、回滚。

而在整个类的第一次实例化时,加载测试数据一次;全部方法执行完毕后,清除数据库。

这样做的好处自不待言,坏处是,在TestCase的方法中,如果用到事务,那么你的测试将出现异常。因为,方法中的事务都被屏蔽了,具体方法可以看django.test.testcases.py的源码,注意这一段:
real_commit = transaction.commit
real_rollback = transaction.rollback
real_enter_transaction_management = transaction.enter_transaction_management
real_leave_transaction_management = transaction.leave_transaction_management
real_savepoint_commit = transaction.savepoint_commit
real_savepoint_rollback = transaction.savepoint_rollback
real_managed = transaction.managed

def nop(*args, **kwargs):
    return

def disable_transaction_methods(): #事务方法都被替换为nop。
    transaction.commit = nop
    transaction.rollback = nop
    transaction.savepoint_commit = nop
    transaction.savepoint_rollback = nop
    transaction.enter_transaction_management = nop
    transaction.leave_transaction_management = nop
    transaction.managed = nop

def restore_transaction_methods(): #恢复事务方法。
    transaction.commit = real_commit
    transaction.rollback = real_rollback
    transaction.savepoint_commit = real_savepoint_commit
    transaction.savepoint_rollback = real_savepoint_rollback
    transaction.enter_transaction_management = real_enter_transaction_management
    transaction.leave_transaction_management = real_leave_transaction_management
    transaction.managed = real_managed


还有这一段:
class TestCase(TransactionTestCase):
    """
    Does basically the same as TransactionTestCase, but surrounds every test
    with a transaction, monkey-patches the real transaction management routines to
    do nothing, and rollsback the test transaction at the end of the test. You have
    to use TransactionTestCase, if you need transaction management inside a test.
    """

    def _fixture_setup(self):
        if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
            return super(TestCase, self)._fixture_setup()

        transaction.enter_transaction_management()
        transaction.managed(True)
        disable_transaction_methods() #禁用事务。

        from django.contrib.sites.models import Site
        Site.objects.clear_cache()

        if hasattr(self, 'fixtures'):
            call_command('loaddata', *self.fixtures, **{
                                                        'verbosity': 0,
                                                        'commit': False
                                                        })

    def _fixture_teardown(self):
        print 'teardown'
        if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
            return super(TestCase, self)._fixture_teardown()

        restore_transaction_methods() #恢复事务。
        transaction.rollback()
        transaction.leave_transaction_management()
        connection.close()


在一个测试方法跟数据库交互的过程中,会多次关闭和创建链接,这是,如果使用了事务,那么关闭链接之前,会有一次commit操作,但是这个操作在TestCase111中被屏蔽了,所以,数据库自行运行了rollback。这就是问题所在。

我们来看看数据库log的输出:
LOG:  duration: 0.209 ms  statement: UPDATE "django_session" SET "session_data" = E'gAJ9cQEoVQp0ZXN0Y29va2llcQJVBndvcmtlZHEDVQ1zaG9wX2NhcnRfa2V5cQRVIDMyYTA4NmQ0
	OGJiZDA5ZGU2MTBiMTg1NmVmMTI1MGM0cQVVEl9hdXRoX3VzZXJfYmFja2VuZHEGVStmb29qaWFf
	d2ViLmRqY29tLmF1dGguYmFja2VuZHMuRW1haWxCYWNrZW5kcQdVDV9hdXRoX3VzZXJfaWRxCEsB
	dS5iM2RjMjhlODVjNTNiOWRhYjFkNDFmMDQ0NmVmNGY2MQ==
	', "expire_date" = E'2010-01-19 23:47:10.886173' WHERE "django_session"."session_key" = E'60c11789a08805fa662ae4a9aabb5d16' 
LOG:  duration: 0.320 ms  statement: ROLLBACK
LOG:  disconnection: session time: 0:00:00.050 user=myweb database=test_myweb host=127.0.0.1 port=58286
LOG:  connection received: host=127.0.0.1 port=58287
LOG:  connection authorized: user=myweb database=test_myweb

这里可以看到,断开链接前,rollback了。
而如果用了TransactionTestCase,就会出现正常的Commit操作,然后再断开。

什么情况下会用到事务?
我在实际测试中发现,其实,包括存储session之类的操作,都需要事务的支持。
除非你测试的内容,是纯读,或者纯写之类的,否则的话,请注意一下TestCase/TransactionTestCase的选择。

你可能感兴趣的:(sql,框架,django,python,PostgreSQL)