本文首发于公众号:Hunter后端

原文链接:Django笔记三十六之单元测试汇总介绍

Django 的单元测试使用了 Python 的标准库:unittest。

在我们创建的每一个 application 下面都有一个 tests.py 文件,我们通过继承 django.test.TestCase 编写我们的单元测试。

本篇笔记会包括单元测试的编写方式,单元测试操作流程,如何复用数据库结构,如何测试接口,如何指定 sqlite 作为我们的单元测试数据库等

以下是本篇笔记目录:

  1. 单元测试示例、使用和介绍
  2. 单元测试流程介绍
  3. 单元测试的执行命令
  4. 复用测试数据库结构
  5. 判断函数
  6. 接口的测试
  7. 标记测试
  8. 单元测试配置
  9. 使用 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_123tests 文件夹下 test 开头的文件,以及 blog 下的 test.pytest_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笔记三十六之单元测试汇总介绍的更多相关文章

  1. Django笔记三十之log日志记录详解

    本文首发于公众号:Hunter后端 原文链接:Django笔记三十之log日志的记录详解 这一节介绍在 Django 系统里使用 logging 记录日志 以下是一个简单的 logging 模块示例, ...

  2. Django笔记二十六之数据库函数之数学公式函数

    本文首发于公众号:Hunter后端 原文链接:Django笔记二十六之数据库函数之数学公式函数 这一篇来介绍一下公式函数,主要是数学公式. 其中 sin,cos 这种大多数情况下用不上的就不介绍了,主 ...

  3. Django笔记三十二之session登录验证操作

    本文首发于公众号:Hunter后端 原文链接:Django笔记三十二之session登录验证操作 这一篇笔记将介绍 session 相关的内容,包括如何在系统中使用 session,以及利用 sess ...

  4. 论文阅读笔记三十六:Mask R-CNN(CVPR2017)

    论文源址:https://arxiv.org/pdf/1703.06870.pdf 开源代码:https://github.com/matterport/Mask_RCNN 摘要 Mask R-CNN ...

  5. 【Unity 3D】学习笔记三十六:物理引擎——刚体

    物理引擎就是游戏中模拟真是的物理效果.如两个物体发生碰撞,物体自由落体等.在unity中使用的是NVIDIA的physX,它渲染的游戏画面很逼真. 刚体 刚体是一个很很中要的组件. 默认情况下,新创的 ...

  6. PHP学习笔记三十六【try 二】

    <?php //定义一个顶级异常处理器 要定义在最上面 function my_exception($e) { echo "我是顶级异常处理:".$e->getMess ...

  7. Django笔记二十八之数据库查询优化汇总

    本文首发于公众号:Hunter后端 原文链接:Django笔记二十八之数据库查询优化汇总 这一篇笔记将从以下几个方面来介绍 Django 在查询过程中的一些优化操作,有一些是介绍如何获取 Django ...

  8. Nodejs学习笔记(十六)--- Pomelo介绍&入门

    目录 前言&介绍 安装Pomelo 创建项目并启动 创建项目 项目结构说明 启动 测试连接 聊天服务器 新建gate和chat服务器 配置master.json 配置servers.json ...

  9. ng机器学习视频笔记(十六) ——从图像处理谈机器学习项目流程

    ng机器学习视频笔记(十六) --从图像处理谈机器学习项目流程 (转载请附上本文链接--linhxx) 一.概述 这里简单讨论图像处理的机器学习过程,主要讨论的是机器学习的项目流程.采用的业务示例是O ...

  10. 《HTTP 权威指南》笔记:第十六章&第十七章 国际化、内容协商与转码

    <HTTP 权威指南>笔记:第十六章 国际化 客户端通过在请求报文中的 Accept-Language 首部和 Accept-Charset 首部来告知服务器:“我理解这些语言.”服务器通 ...

随机推荐

  1. DRF_视图类

    drf 视图组件 视图基类 基于APIView写五个接口 基于GenericAPIView写5个接口 5个视图扩展类 9个视图子类 视图集 两个视图基类 视图的两个基类分别是 ​ APIView : ...

  2. 摘记:找到程式中 不會被執行到的 code

    定义 https://en.wikipedia.org/wiki/Unreachable_codeRemove unreachable code refactoringhttps://docs.mic ...

  3. redis保存数据

    转载: https://blog.csdn.net/y510662669/article/details/106677717

  4. k8sdeploy配置文件示例

    apiVersion: extensions/v1beta1 kind: Deployment metadata: name: [k8s服务名] namespace: default labels: ...

  5. MySQL学习(二)事务的隔离级别

    :规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的.较低级别的隔离通常可以执行更高的并发,系统的开销也更低 read uncommitted(未提交读):事务中的修改,即使没有 ...

  6. 人脸关键点的应用场景及重难点解析丨Dev for Dev 专栏

    本文为「Dev for Dev 专栏」系列内容,作者为声网视频组 AI 算法工程师 周世付. 人脸检测.人脸关键点检测,是计算机视觉的基础算法.许多酷炫应用背后,例如美颜.贴纸.人脸驱动 avatar ...

  7. 百炼成钢 —— 声网实时网络的自动运维丨Dev for Dev 专栏

    本文为「Dev for Dev 专栏」系列内容,作者为声网大数据算法工程师黄南薰. 01 自动运维介绍 2016 年,Gartner 创新性地提出了 AIOps 的概念[1],开创了人工智能辅助运维决 ...

  8. 初学J2V8

    V8和J2V8 V8 V8是Google开源的JavaScript和WebAssembly引擎,被用于Chrome浏览器和Node.js等.和其它JavaScript引擎把JavaScript转换成字 ...

  9. Java面试——TCP与HTTP

    更多内容,移步 IT-BLOG 一.Session 和 Cookie 的区别 [1]Cookie 保存在客户端,未设置存储时间的 Cookie,关闭浏览器会话 Cookie 就会被删除:设置了存储时间 ...

  10. Java面试——锁

    公平锁:是指多个线程按照申请锁的顺序来获取锁,有点先来后到的意思.在并发环境中,每个线程在获取锁时会先查看此锁维护的队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中, ...