Django Testing

Django 自己的 unittest 支持的挺好,一般只需要在 app 下面加一个 tests.py 在里面写 case 就可以了。case 对应的类继承 django.test.TestCase 就好。

这个 django.test.TestCase 继承自 unittest.TestCase ,django 这个多了一个自动使用事务的功能,所以用 django 这个的话,每个测试用例执行前后会自动回滚数据库操作,这样不用你自己 cleanup 数据,还比较方便。

setUp 和 tearDown

每一个测试类里面,都可以有一个 setUp 方法,是在 case 方法执行前执行,例如一些准备工作,和一个 tearDown 方法,在 case 执行之后执行,例如一些清理工作。还可以有若干个使用 test_ 开头的测试用例,这些 setUp 其实类似于把每个测试用例里面共同的部分提取出来一样,不过是不用你在每个 case 里面单独调用了,会自动处理。

setUpClass 和 tearDownClass

django 还提供了 setUpClasstearDownClass ,类似上面的 setUp 方法,不过这个是每个 class 只会执行一次。另外按照这里的说法, setUpClass 不会使用事务,不过我看源码(django 2.1.4) 好像是会的,我没测试。。

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        if not connections_support_transactions():
            return
        cls.cls_atomics = cls._enter_atomics()

        if cls.fixtures:
            for db_name in cls._databases_names(include_mirrors=False):
                try:
                    call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name})
                except Exception:
                    cls._rollback_atomics(cls.cls_atomics)
                    raise
        try:
            cls.setUpTestData()
        except Exception:
            cls._rollback_atomics(cls.cls_atomics)
            raise

代码可以看到,还有一个 setUpTestData 可以用。如果只是准备数据库数据的话,感觉后面这个更精准一点。

测试用例

case 我觉得一般可以分两种,方法测试,和接口测试。

方法测试指针对一些工具方法什么的测试,当然这个说法并不严谨,将就理解吧。我把这些归类为不涉及到数据库操作的测试。

接口测试,一般会涉及到数据库操作,需要验证登录啊,参数什么的。

Django 里面,每个测试用例之间是通过事务互相隔离的,所以不用担心互相之间会有影响。

接口测试可以通过 django.test.Client 来访问你的接口,然后比对返回结果,或者比对数据库的数据来验证。

fixtures

有时候一些接口是依赖已有数据的,比如一个返回所有用户的接口,那测试的时候数据库是需要有用户才能返回的。这个可以通过 fixture 来 moke 数据。

fixture 就是一些 json 文件,里面放的是和 model 的数据,这样一个测试如果需要某几个 model 对应的表里面事先有数据,那可以把他们放到 fixture 文件里面,让 django 在运行之前先 load 到数据库就可以了。

这些 json 文件自己编写会死,Django 提供了 manage.py dumpdata --indent 4 [app_label[.ModelName] [app_label[.ModelName] 功能,可以方便你导出数据库里面已有的数据。不指定 app_label 和 modelname 就会导出全部的,一般只导出自己需要的就好。注意 json 文件是可以支持缩进的。

如果从比如开发库之类的倒数据,会觉得数据有点乱,从测试库倒数据似乎比较清净,因为每次测试都是一个空的数据库。有一个方法是在测试用例里面创建依赖的数据,但是测试执行完了再执行 manage.py dumpdata 已经什么数据都没有了。这个时候可以在测试用例里面使用 django.core.management.call_command 来执行 dump,例如

call_command(
    'dumpdata',
    '--indent', '4',
    'app_label', 'ModelName',
    'app_label', 'ModelName'
)

这个其实类似前面 setUpClass 里面的加载 fixtures 的代码用了类似的方法 call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name})

命令行执行测试的时候,会打印出来 dump 出来的数据,把他们存到一个 json 文件然后引入就可以了。

给测试提速

有一个提升测试速度的方法,是使用 -k 参数,这个参数会保留测试的数据库,不会每次都删除重建,这样节省一些时间。

还可以使用 --parallel N 参数来增加并行数量。如果你用 coverage 那整个命令是类似这样的 coverage run --parallel-mode --concurrency=multiprocessing manage.py test -k --parallel 3 。使用并行之后,会发现 coverage 不工作了,这是因为并行的时候,每个线程都会单独写一个 coverage 结果文件,所以执行 coverage report 之前,执行一下 coverage combine 合并到一个文件就可以了。

Django 还是做的挺不错的。测试这么方便,实际很适合使用 TDD 方式开发。

其他

看到一篇文章,从 TransactionTestCase 替换到 TestCase 速度提升了 3 倍。

Django 里面有三个测试类

  1. SimpleTestCase 是最简单的。提供 unittest.TestCase 基础的功能。默认屏蔽了数据库的访问,因为对数据库里面的修改没有隔离,所以应该在没有数据库操作的时候使用他。
  2. TransactionTestCase 继承自 SimpleTestCase ,允许数据库操作,测试完毕之后会删除数据库里面的所有数据。会比较慢。
  3. TestCase 是我们平时用的。继承自 TransactionTestCase ,使用事务来回滚所有操作,这样比遍历所有表快一点,这样你的操作也不能真实的提交,但是我们跑测试的时候一般也不需要提交。

TransactionTestCase 允许你的代码自己使用和管理事务, TestCase 自己使用了事务。