共计 9445 个字符,预计需要花费 24 分钟才能阅读完成。
Django 的单元测试应用了 Python 的规范库:unittest。
在咱们创立的每一个 application 上面都有一个 tests.py 文件,咱们通过继承 django.test.TestCase 编写咱们的单元测试。
本篇笔记会包含单元测试的编写形式,单元测试操作流程,如何复用数据库构造,如何测试接口,如何指定 sqlite 作为咱们的单元测试数据库等
以下是本篇笔记目录:
- 单元测试示例、应用和介绍
- 单元测试流程介绍
- 单元测试的执行命令
- 复用测试数据库构造
- 判断函数
- 接口的测试
- 标记测试
- 单元测试配置
- 应用 SQLite 作为测试数据库
1、单元测试示例、应用和介绍
首先咱们编写 blog/tests.py 文件,创立一个简略的单元测试:
from django.test import TestCase | |
from blog.models import Blog | |
class BlogCreateTestCase(TestCase): | |
def setUp(self): | |
Blog.objects.create(name="Python", tag_line="this is a tag line") | |
def test_get_blog(self): | |
blog = Blog.objects.get(name="Python") | |
self.assertEqual(blog.name, "Python") |
以上是一个很简略的单元测试示例,接下来咱们执行这个单元测试:
python3 manage.py test blog.tests.BlogCreateTestCase.test_get_blog
执行之后能够看到控制台会输入一些信息,如果没有报错,阐明咱们的这个单元测试胜利执行。
在 BlogCreateTestCase 中,这个单元测试继承了 django.test.TestCase,咱们在 setUp() 函数中执行一些操作,这个操作会在执行某个测试,比方 test_get_blog() 前先执行。
咱们执行的是 test_get_blog() 函数,这里的逻辑是先获取一个 blog 示例,而后通过 assertEqual() 函数判断两个输出的值是否相等,如果相等,则单元测试通过,否则会报失败的谬误。
2、单元测试流程介绍
首先咱们看一下 settings.py 中的数据库定义:
# hunter/settings.py | |
DATABASES = { | |
'default': { | |
'ENGINE': "django.db.backends.mysql", | |
'NAME': "func_db", | |
"USER": "root", | |
"PASSWORD": "123456", | |
"HOST": "192.168.1.9", | |
"PORT": 3306, | |
}, | |
} |
当咱们执行上面这个命令之后:
python3 manage.py test blog.tests.BlogCreateTestCase.test_get_blog
零碎会去 default 这个数据库的连贯地址,创立一个新的数据库,数据库名称为以后数据库的名称加上 test_
前缀。
比方咱们连贯的正式数据库名称为 func_db
,那么测试数据库名为 test_func_db
。
创立该数据库之后,零碎会将以后零碎所有的 migration 都执行一遍到测试数据库,而后根据咱们单元测试的逻辑,比方 setUp() 中对数据的初始化,以及 test_get_blog() 中对数据的获取和比拟操作执行一遍逻辑。
这个流程完结之后,零碎会主动删除刚刚创立的测试数据库,至此,一个单元测试执行的流程就完结了。
3、单元测试的执行命令
执行单个单元测试
下面咱们执行的单元测试的命令准确到了类中的函数,咱们也能够间接执行某个单元测试,比方咱们的 BlogCreateTestCase 内容如下:
class BlogCreateTestCase(TestCase): | |
def setUp(self): | |
Blog.objects.create(name="Python", tag_line="this is a tag line") | |
def test_get_blog(self): | |
print("test_get_blog") | |
def test_get_blog_2(self): | |
print("test_get_blog_2") |
咱们间接执行命令到这个单元测试:
python3 manage.py test blog.tests.BlogCreateTestCase
那么零碎就会执行 BlogCreateTestCase 下 test_get_blog 和 test_get_blog_2 这两个函数。
执行单元测试文件
再往上一层,咱们能够执行某个单元测试的文件,比方该 tests.py 内容如下:
# blog/tests.py | |
class BlogCreateTestCase(TestCase): | |
def setUp(self): | |
Blog.objects.create(name="Python", tag_line="this is a tag line") | |
def test_get_blog(self): | |
print("test_get_blog") | |
class BlogCreateTestCase2(TestCase): | |
def test_get_blog_2(self): | |
print("test_get_blog_2") |
当咱们执行:
python3 manage.py test blog.tests
零碎就会将 tests.py 中 BlogCreateTestCase 和 BlogCreateTestCase2 这两个单元测试都执行一遍。
执行零碎所有单元测试
如果咱们想要对立执行零碎全副单元测试,能够间接如下操作:
python3 manage.py test
单元测试查找逻辑
当咱们执行下面那条命令的时候,零碎是如何查找处测试文件的呢?
零碎会搜寻目录下所有 test 结尾的文件夹或者文件,如果是文件夹,则持续寻找文件夹下 test 结尾的文件,对于每个 test 结尾的文件,找到继承了 django.test.TestCase 的类,而后执行每个结尾名为 test 的类函数。
接下来咱们举几个示例,假如咱们在 blog 的目录下有这样的构造:
blog/ | |
test_123/ | |
no_test.py | |
test_ok.py | |
tests.py | |
tests/ | |
tests.py | |
test_123.py | |
no_test/ | |
test_123.py | |
test.py | |
test_123.py | |
no_test.py |
在下面这个目录构造下,零碎会去搜寻 test_123
和 tests
文件夹下 test
结尾的文件,以及 blog
下的 test.py
、test_123.py
,寻找其中继承了 django.test.TestCase
的类作为单元测试而后执行。
在这里,比方 test_123/no_test.py
这个文件就不会被断定为测试文件,因为它名称不是 test
结尾的。
而在 test
结尾的测试文件中,如果一个类继承了 django.test.TestCase
,然而它的类函数并不是以 test
结尾的,这样的函数也不会被执行,比方:
class BlogCreateTestCase(TestCase): | |
def setUp(self): | |
Blog.objects.create(name="如何 Python", tag_line="this is a tag line") | |
def test_ok(self): | |
print("12344444............") | |
self.assertEqual(1, 1) | |
def no_test(self): | |
print("no test") |
比方下面这个单元测试,test_ok
这个类函数就会被作为单元测试的一部分,而 no_test
则不会被执行。
如果测试文件较多,为了对立治理,咱们能够都放在 application 下的 tests 文件夹下,比方:
blog/ | |
tests/ | |
test_1.py | |
test_2.py | |
test_3.py |
4、复用测试数据库构造
当咱们写完一个性能,而后编写这个性能的单元测试,紧接着去测这个单元测试,零碎就会去创立一个数据库,而后执行所有的 migration,而后执行单元测试逻辑,执行完结之后会删掉该测试数据库。
在咱们的我的项目中,如果保护到了前期,领有的 migration 较多,每次执行单元测试都要删掉而后重建数据库,在工夫上是一个很大的耗费,那么咱们如何在执行完一个单元测试之后保留以后的测试数据库用于下一次执行呢。
那就是应用 --keepdb
参数。
依照后面的逻辑,咱们的测试数据库会在 DATABASES 中定义的数据库地址新建一个数据库,咱们能够应用 –keepdb 执行这样的操作:
python3 manage.py test --keepdb blog.tests.BlogCreateTestCase
加上 –keepdb 参数之后,执行单元测试完结之后,咱们能够通过 workbench 或者 navicat 等工具去该数据库地址查看,会多出一个名为 test_fund_db
的数据库,那就是咱们执行单元测试之后没有删除的测试数据库。
当咱们下次再执行这个或者其余单元测试的时候,能够发现执行的工夫就变得很快了,而且在控制台会输入这样一条信息:
Using existing test database for alias 'default'...
意思就是应用曾经存在的测试数据库。
而不加 –keepdb 的时候,输入的是:
Creating test database for alias 'default'...
示意的是正在创立新的测试数据库。
留神: 尽管单元测试完结之后数据库的构造还会保留,然而在单元测试中咱们创立的数据还是会被删除。这个仅限于在单元测试中创立的数据,通过 migration 初始化的数据还是存在数据库中。
5、判断函数
在介绍测试接口前,咱们先介绍一下几个断定函数。
self.assertEqual
这个函数接管三个参数,前两个参数用于比拟是否相等,第三个参数为 msg,用于在前两个参数不相等时报出的错误信息,然而可不传,默认为 None。
比方咱们这样操作:
self.assertEqual(Blog.objects.count(), 20, msg="blog count error") | |
self.assertEqual(Blog.objects.count(), 20) |
如果前两个参数不相等则单元测试会不通过。
self.assertTrue
这个函数接管两个参数,前一个参数是一个表达式,后一个参数是 msg,也是用于前一个参数不为 True 的时候报出的错误信息,可不传,默认为 None。
咱们能够这样操作:
self.assertTrue(Blog.objects.filter(name="Python").exists(), "Pyrhon blog not exists") | |
self.assertTrue(Blog.objects.filter(name="Python").exists()) |
同样,如果表达式参数不为 True,则单元测试不会通过。
self.assertIn
接管三个参数,如果第二个参数不蕴含第一个参数,则会报错,比方:
self.assertIn(6, [1,2,3], "not in list") | |
self.assertIn("a", "def", "not in string") |
self.assertIsNone
接口两个参数,示意如果传入的参数为 None 则通过单元测试:
a = None | |
self.assertIsNone(a) |
对于 assertEqual、assertTrue、assertIn、assertIsNone 还有对应的相同意义的函数
- assertNotEqual 示意断定两者不相等
- assertFalse 示意断定表达式为 False
- assertNotIn 示意断定后者不蕴含前者
- assertIsNotNone 示意断定不为 None
这里还有一些断定大于、小于、大于等于、小于等于的函数,这里就不做多介绍了 assertGreater、assertLess、assertGreaterEqual、assertLessEqual
self.fail(msg=”failed testcase”)
如果咱们心愿在某些判断条件下间接让单元测试不通过,能够间接应用 self.fail() 函数,比方:
a = 1 | |
b = 2 | |
if a < b: | |
self.fail(msg="a < b") |
6、接口的测试
在下面咱们的单元测试中,咱们应用的只是简略的对于 model 的创立查问和验证,然而一般来说,除了测试零碎的工具类函数,咱们罕用到的测试用处是测试和验证接口的逻辑。
在介绍如何对接口进行测试前,一下 model_mommy 库。
model_mommy 库
这是个能够模仿 model 数据的库,它有什么用途呢,比方咱们想创立几条 model 的数据,然而不关怀一些必填字段的值,或者只想指定某几个字段特定的值,或者想批量创立某个 model 的数据。
首先咱们引入这个库:
pip3 install model_mommy
应用 model_mommy
来创立模仿数据:
from model_mommy import mommy | |
blog_1 = mommy.make(Blog, name="Python") |
这样咱们就创立了一条数据,这个时候如果咱们打印出 blog_1 的内容,能够发现 Blog 的有默认值的字段都被默认值填充,无默认值的都会被无意义数据填充
print(blog_1.__dict__) | |
# 'id': 4, 'name': 'Python', 'tag_line': 'sIDENcYqKVwESvEUAwZGIVtGdWHhKyNNoDzoaZCdDuqQuIKCkwazqwfcNEEtzfcoZeEnVVDiVLzAhhOuYsxiuKUOVFifUimnCLbMNHMpYLYxHCVSVfiggeBQhmRPFuIUwiKDUSDZztzQzFlKfcSxdnewsekQBzlCuMZLVPyOrfTXYWgPIkBhytzBkcMbpvCvidSETxZRjWeeEBPLELHpHYOmKgKHdNxrmjjLlewGWKTLQNFPFWOGndzncghTEcuFnEfRQvGgXcsPTfaGAHDDqPGyNeerTmOHDTUmnWmzHIXF', 'char_count': 0, 'is_published': 0, 'pub_datetime': None} |
或者咱们想批量创立二十条 Blog 的数据,咱们能够通过 _quantity
参数这样操作:
mommy.make(Blog, _quantity=20)
Client() 调用接口
调用接口用到的函数是 Client()
假如咱们想要调用登录接口,咱们能够如下操作:
from django.test import Client | |
url = "/users/login" | |
c = Client() | |
response = c.post(url, data={"username": "admin", "password": "123456"}, content_type="application/json") | |
self.assertEqual(response.json().get("code"), 0) |
应用单元测试而不是应用 postman 调用有一个益处就是咱们不必把后端服务启动起来,所以这里的 url 相应的也不必加上 ip 地址或者域名。
调用接口还有另一种形式,就是在继承了 django.test.TestCase
的单元测试中间接应用 self.client
,它与实例化 Client()
后的间接作用成果是一样的,都能够用来调用接口。
那为什么要应用 self.client
呢,是为了主动保留登录接口的 session。
比方对于 /users/user/info
这个须要登录后能力拜访到的用户信息接口,咱们就能够应用 self.client
在 setUp() 初始化数据的时候先进行登录操作,接着就能够以已登录状态拜访用户信息接口了。
class UserInfoTestCase(TestCase): | |
def setUp(self): | |
username = "admin" | |
password = make_password("123456") | |
User.objects.create(username=username, password=password) | |
url = "/users/login" | |
response = self.client.post(url, data={"username": "admin", "password": "123456"}, content_type="application/json") | |
resp_data = response.json() | |
print("login...") | |
self.assertEqual(resp_data.get("code"), 0) | |
def test_user_info(self): | |
url = "/users/user/info" | |
response = self.client.post(url) | |
print(response.json()) |
如果零碎大部分接口都须要以登录状态能力拜访,咱们甚至能够将登录操作写入一个根底类,其余的单元测试都继承这个类,这样就不须要反复编写登录的接口了:
class BaseTestCase(TestCase): | |
def setUp(self): | |
username = "admin" | |
password = make_password("123456") | |
User.objects.create(username=username, password=password) | |
url = "/users/login" | |
response = self.client.post(url, data={"username": "admin", "password": "123456"}, content_type="application/json") | |
resp_data = response.json() | |
print("login...") | |
self.assertEqual(resp_data.get("code"), 0) | |
class UserInfoTestCase(BaseTestCase): | |
def test_user_info(self): | |
url = "/users/user/info" | |
response = self.client.post(url) | |
print(response.json()) | |
class TestCase2(BaseTestCase): | |
def test_case(self): | |
url = "/xx/xxx" | |
response = self.client.post(url) | |
print(response.json()) |
7、标记测试
一般来说,咱们的单元测试是都要全副通过能力上线进入生产环境的,然而某些状况下,咱们对系统只进行了少部分的批改,或者说只须要测试某些特定的重要性能就能够上线,这种状况下能够给咱们的测试用例打上 tag,这样在测试的时候就能够筛选特定的单元测试,通过即可上线。
这个 tag 能够打到一个单元测试上,也能够打到某个单元测试的函数上,比方咱们有三个标记,fast,slow,core,以下是几个单元测试:
from django.test import tag | |
class SingleTestCase(TestCase): | |
@tag("fast", "core") | |
def test_1(self): | |
print("fast, core from SingleTestCase.test_1") | |
@tag("slow") | |
def test_2(self): | |
print("slow from SingleTestCase.test_2") | |
@tag("core") | |
class CoreTestCase(TestCase): | |
def test_1(self): | |
print("core from CoreTestCase") |
而后咱们能够通过 –tag 指定标记的单元测试:
python3 manage.py test --keepdb --tag=core | |
python3 manage.py test --keepdb --tag=core --tag=slow |
8、单元测试配置
编码配置
在后面咱们的数据库链接中,并没有指定数据库的编码,而咱们创立生产数据库的时候应用的 charset 是 utf-8,而测试数据库在创立的时候没有指定编码的话,默认应用的是 latin1 编码。
这样会造成一个问题,就是咱们的单元测试在往数据库写入数据的时候就会因为不反对中文而导致报错。
比方在不设置编码的时候咱们应用上面的单元测试就会报错:
from django.test import TestCase | |
from blog.models import Blog | |
class BlogCreateTestCase(TestCase): | |
def setUp(self): | |
Blog.objects.create(name="测试数据", tag_line="this is a tag line") | |
def test_get_blog(self): | |
blog = Blog.objects.get(name="测试数据") | |
self.assertEqual(blog.name, "测试数据") |
所以如果要指定创立的测试数据库的编码,咱们须要加上一个配置:
DATABASES = { | |
'default': { | |
... | |
"TEST": {"CHARSET": "utf8",}, | |
} | |
} |
测试数据库名称
默认状况下,测试数据库的名称是 'test_'
+ DATABASES['default']['name']
,如果咱们想指定测试数据库名称,能够额定加一个 NAME 字段:
DATABASES = { | |
'default': { | |
... | |
"TEST": { | |
"CHARSET": "utf8", | |
"NAME": "test_default_db", | |
}, | |
} | |
} |
9、应用 SQLite 作为测试数据库
目前咱们的测试数据库是在 default 数据库的地址新建一个数据库,如果咱们想要运行单元测试的时候间接在本地应用 SQLite 作为咱们的测试数据库,能够在 settings.py 中定义 DATABASES 的前面加上上面的定义:
import sys | |
if "test" in sys.argv: | |
DATABASES = { | |
"default": { | |
"ENGINE": "django.db.backends.sqlite3", | |
"NAME": os.path.join(BASE_DIR, "db.sqlite3"), | |
"TEST": {"NAME": os.path.join(BASE_DIR, "test_db.sqlite3"), | |
} | |
} | |
} |
其中,sys.argv 是一个列表,列表元素是咱们执行命令的各个参数。
所以当咱们执行单元测试命令的时候,会蕴含 test
,所以数据库的链接内容就会走咱们这个逻辑。
在这部分,咱们应用 ENGINE 来确定了后端数据库的类型为 SQLite,而后通过 DATABASES["default"]["test"]["NAME"]
来指定咱们的测试数据库地址。
当咱们执行单元测试的命令时,在零碎根目录下就会多出一个 test_db.sqlite3
的数据库。
本文首发于自己微信公众号:Django 笔记。
原文链接:Django 笔记三十六之单元测试汇总介绍
如果想获取更多相干文章,可扫码关注浏览: