Django单元测试

测试例子部分源于官方文档,部分为自己碰到的案例,官方文档链接:https://docs.djangoproject.com/en/2.2/topics/testing/

除了官方文档提到的内容,本文主要写一下自己使用过程中碰到的问题和解决方法

0X01 如何编写单元测试

Python有unittest库来进行单元测试,Django的单元测试是基于unittest库的,只不过在unittest的基础上进行了封装,并提供了一系列单元测试工具,来帮助你进行测试。Django在创建APP之后,默认会在APP目录下创建一个tests.py文件,这里就是存放你测试代码的地方,当然,如果需要测试的内容多了,都放在一个文件中显然不合理,所以Django提供高级玩法,模块化测试,这个后面再说,先上一个例子:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

上面就是一个最基础的测试用例,你所编写的测试类需要继承于TestCase,当你运行测试时,Django会找到所有的以test开头的文件,运行这些文件中所有父类是TestCase的类中的测试方法,测试方法为以test开头的函数。

上面的例子中,setUp函数会在运行test_xxxx函数前被调用,可以进行一些初始化操作,例如创建测试数据,获取Token等。

0X02 运行单元测试

在Django项目目录下使用manage.py来运行单元测试,该命令会运行所有APP的所有tests.py中的所有测试:

python manage.py test

运行某个package下的所有测试:

python manage.py test package_name

运行某个package下的某个测试类(TestCase):

python manage.py test pack.tests.xxxTestCase

运行某个package下的某个测试类的某个测试方法:

python manage.py test pack.tests.xxxTestCase.test_animals_can_speak

上面提到过,默认会匹配所有以test_func开头的测试方法,你也可以自己制定测试方法的正则:

python manage.py test --pattern="tests_*.py"

在运行单元测试的时候,可以使用ctrl+c来结束测试,测试程序将等待当前正在运行的测试结束,然后正常退出。在退出期间,测试程序会和往常一样输出测试失败的详细信息、运行了多少测试、出现的错误和故障,并且会像往常一样销毁测试数据库。

连续按下两次ctrl+c将会强制退出测试程序,此时会缺少一些详细信息,也不会销毁测试数据库,慎用。

0X03 测试数据库

需要和model交互的话会需要用到数据库,但Django不会使用生产环境的数据库进行测试,会创建单独的测试数据库,例如settings.py中定义的连接的数据库为product_db,那么默认创建的测试数据库就为test_product_db。

上面提到过,在执行单元测试命令后,会自动创建测试数据库,结束测试后会自动销毁测试数据库。每次都去创建和销毁比较耗时,如果不需要销毁测试数据库可以携带keepdb参数,不销毁数据库:

python manage.py test --keepdb

如上一节所述,如果强制中断测试运行,则可能不会销毁测试数据库。在下一次运行时,将询问您是否要重用或销毁数据库。使用该选项可以禁止该提示并自动销毁数据库。例如,在连续集成服务器上运行测试时,这可能很有用,例如,测试可能会因超时而中断:

python manage.py test --noinput

默认Django settings中指定的是sqlite数据库,那么测试数据库将创建在内存中,不会使用文件系统。在settings.py中的DATABASES提供了很多关于测试数据库的选项,例如指定测试数据库的名字:、

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'USER': 'mydatabaseuser',
        'NAME': 'mydatabase',
        'TEST': {
            'NAME': 'mytestdatabase',
        },
    },
}

更多字段:https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-TEST_NAME

有一个地方暂时没有捋清楚什么意思,如果有兄弟知道可以分享下,原文:https://docs.djangoproject.com/en/2.2/ref/applications/#django.apps.AppConfig.ready

Finding data from your production database when running tests?

If your code attempts to access the database when its modules are compiled, this will occur before the test database is set up, with potentially unexpected results. For example, if you have a database query in module-level code and a real database exists, production data could pollute your tests. It is a bad idea to have such import-time database queries in your code anyway - rewrite your code so that it doesn’t do this.

This also applies to customized implementations of ready().

0X04 关于Model测试

涉及到Model的测试,有一个需要注意的地方,每个测试方法都是独立的,比如编写了以下测试类,在测试方法A创建的对象,用测试方法B去获取,是获取不到的,在测试方法A结束后该对象就被销毁了,具体例子:

class UserTestCase(TestCase):

    def setUp(self):
        UserInfo.objects.create(username='sup_user', password='123456')

    def test_add_user(self):
        UserInfo.objects.create(username='user', password='123456')

    def test_sup_user_exist(self):
        self.assertEqual(UserInfo.objects.filter(username='sup_user').count(), 1)

    def test_user_exist(self):
        self.assertEqual(UserInfo.objects.filter(username='user').count(), 1)

上述代码在setUp方法中创建了sup_user用户,在test_add_user方法中创建了user用户,然后test_sup_user_exist方法测试sup_user是否存在,test_user_exist方法测试user用户是否存在,测试结果如下:

Django单元测试_第1张图片

可以看出来,setUp方法创建的对象生命周期是整个测试类的运行过程,test方法中的对象生命周期仅仅是这个方法。当然本类setUp创建的对象,在其他类也是查询不到的,数据库一直为空,实测得出的结论。

0X05 API的测试

Django提供了Client来进行API测试,https://docs.djangoproject.com/en/2.2/topics/testing/tools/

>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200

0X06 执行测试的顺序

1.首先执行所有TestCase的子类

2.接着执行SimpleTestCase和TransactionTestCase的子类

3.最后执行unittest.TestCase和doctests

0X07 rest_framework单元测试

实际场景中经常会用到rest_framework和rest_framework_jwt。那么所有的API都是需要进行身份验证的,那么该如何进行单元测试呢?官方文档:https://www.django-rest-framework.org/api-guide/testing/

1.APIRequestFactory

DjangoRequestFactory的子类,具体的参数和用法查阅文档即可,下面是一个DEMO:

from rest_framework.test import APIRequestFactory

class InterfaceTestCase(TestCase):

    def setUp(self):
        self.user = UserInfo.objects.create_user(username='sup_admin', password='123456')

    def test_list_method(self):
        factory = APIRequestFactory()
        request = factory.get('/interfaces/')
        view = VIEWSET_CONF['interface_list'] # viewset.as_view({'get': 'list'})
        force_authenticate(request, user=self.user)
        response = view(request)
        self.assertEqual(response.status_code, 200)

首先创建APIRequestFactory对象,调用get方法,返回一个WSGIRequest对象,实例化处理请求的视图函数对象并调用,返回response。可以看到在setUp方法里我创建一个用户,然后在test方法里使用force_authenticate进行强制身份验证,这样就可以正常访问视图了。

2.APIClient

扩展了Django的Client,DEMO:

from rest_framework.test import APIClient

class InterfaceTestCase(TestCase):

    def setUp(self):
        self.user = UserInfo.objects.create_user(username='sup_admin', password='123456')
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_list_method(self):
        response = self.client.get('/interfaces/')
        self.assertEqual(response.status_code, 200)

文档中提到了使用client.login方法来通过验证,但是该方法只支持SessionAuthntication,JWT的话不支持。

self.client.login(username=self.user.username, password=self.user.password)

3.RequestsClient

有别于上面提到的所有方法,该方法是完全模拟一个外部请求的请求过程,使用的是Python的requests库,这个使用起来注意事项很多,有需求自行查阅文档

你可能感兴趣的:(Django,测试,Django,单元测试,Python,Web)