当前位置:首页>焦点 > 正文

全球观天下!Django笔记三十六之单元测试汇总介绍

  • 2023-05-07 04:45:12来源:博客园

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


【资料图】

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

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

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

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

以下是本篇笔记目录:

单元测试示例、使用和介绍单元测试流程介绍单元测试的执行命令复用测试数据库结构判断函数接口的测试标记测试单元测试配置使用 SQLite 作为测试数据库1、单元测试示例、使用和介绍

首先我们编写 blog/tests.py 文件,创建一个简单的单元测试:

from django.test import TestCasefrom blog.models import Blogclass 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.pyDATABASES = {    "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.pyclass 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 = Noneself.assertIsNone(a)

对于 assertEqual、 assertTrue、assertIn、assertIsNone 还有对应的相反意义的函数

assertNotEqual 表示判定两者不相等assertFalse 表示判定表达式为 FalseassertNotIn 表示判定后者不包含前者assertIsNotNone 表示判定不为 None

这里还有一些判定大于、小于、大于等于、小于等于的函数,这里就不做多介绍了 assertGreater、assertLess、assertGreaterEqual、assertLessEqual

self.fail(msg="failed testcase")

如果我们希望在某些判断条件下直接让单元测试不通过,可以直接使用 self.fail() 函数,比如:

a = 1b = 2if 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 mommyblog_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 Clienturl = "/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 tagclass 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=corepython3 manage.py test --keepdb --tag=core --tag=slow
8、单元测试配置

编码配置

在前面我们的数据库链接中,并没有指定数据库的编码,而我们创建生产数据库的时候使用的 charset 是 utf-8,而测试数据库在创建的时候没有指定编码的话,默认使用的是 latin1 编码。

这样会造成一个问题,就是我们的单元测试在往数据库写入数据的时候就会因为不支持中文而导致报错。

比如在不设置编码的时候我们使用下面的单元测试就会报错:

from django.test import TestCasefrom blog.models import Blogclass 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 sysif "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笔记三十六之单元测试汇总介绍

本文首发于公众号:Hunter后端原文链接:Django笔记三十六之单元测试汇总介绍Django的单元测试使用了Python

小孩子流鼻涕怎么办速效办法_小孩子流鼻涕怎么办 即时看

1、孩子有流鼻涕的症状,考虑到身体偏冷,风寒感冒的可能性较大。2、如果孩子总是流鼻涕,没有感冒的症状,

茂名市电白区沙院中学_关于茂名市电白区沙院中学的简介

音频解说1、沙院中学创办于1965年,前身为沙院农中。五十一年的风雨兼程,历经黄琴光、李广潮等十任校长的

医用不锈钢是304还是316_医用不锈钢 焦点滚动

1、高氮BIODUR108不锈钢是机械和物理性能更优的医疗植入用钢与其他医疗用钢相比,为医疗植入设计开发的无镍

7大营养素的主要功能_7大营养素-世界观热点

1、人类日常的饮食含有七大营养素,分别是碳水化合物(糖类)、蛋白质、脂肪、矿物质、纤维、维生素和水,

惧怕中的惧的意思是什么_惧怕中的惧是什么意思

1、恐惧中的恐惧是指:恐惧。2、恐惧(拼音:j)是《汉语通用规范》中的一类词(常用词)。这个词最早出现在战

第二届“滤泡性淋巴瘤患者公益日”宣教活动在西京医院举办 环球快看点

5月6日,第二届“滤泡性淋巴瘤患者公益日”公众宣教、义诊活动在西京医院举办。西京医院血液科团队高广勋带

中储智运“数字供应链”加速探索中国式现代化物流新实践-环球速讯

社会物流总费用占GDP的比率是当前国际公认的物流成本衡量标准。公开数据显示,“十三五”期间,我国社会物

绿色智能制造创赢计划第四季启动 加速低代码、AIGC等技术创新落地工业场景 全球讯息

近日,由工业和信息化部国际经济技术合作中心与施耐德电气共同主办的“绿色智能制造创赢计划”第四季正式启

行业首发!雷鸟科技“超级智绘”AI故事集上线TCL电视-环球聚看点

【CNMO新闻】随着科技的不断发展,AI在各个领域得到越来越广泛的应用,它正逐渐成为我们生活中不可或缺的一

灵璧技工学校项目部开展防汛应急演练

为进一步提高项目部处置汛期突发事件的应急救援能力,做好防汛抢险各项工作,确保群众生命财产安全。近日,

五香酥鱼带鱼的做法?-每日观点

第一步,买来新鲜带鱼,要越窄越薄的为好,洗净,切成寸段,放入盆中,撒盐腌滓1到2小时。第二步,锅内放油

浏阳市委编办高质量推动机构编制工作上水平

近年来,浏阳市委编办始终坚持把调研作为推动机构编制工作高质量发展的有效途径,通过创新“内部浸入”“外

焦点速读:华泰证券:4月美国非农强于预期 但或许和季节性有关

【华泰证券:4月美国非农强于预期但或许和季节性有关】财联社5月6日电,华泰证券研报指出,4月美国就业报告

世界观天下!65至74岁退休养老金多少钱?养老金是怎么计算的?

一、65至74岁养老金多少钱要看实际情况。每个人每个地方的基本养老金的领取标准是不一样的,不同的基本养老

什么时候可以买2023连云港薛之谦天外来物演唱会门票? 环球今热点

薛之谦“天外来物”巡回演唱会-连云港站门票2023年5月9日17时17分开抢!具体本文为您介绍。

乌克兰总统泽连斯基与法国总统马克龙通话

乌克兰总统泽连斯基当地时间11月1日在其官方推特上宣布,当天他与法国总统马克龙通了电话,并表示与马克龙

视焦点讯!五一超20万人去淄博 怎么可以错过

原标题:淄博烧烤火出圈,五一期间超12万人去吃烧烤,淄博:不涨价还便宜淄博烧烤火出圈,五一期间超12万人

沪指涨0.22% 权重股港股开户-上艾德证券表现亮眼

今天沪深两市双双低开,盘初震动下行,沪指最低下探至3087点,随后钢铁、煤炭、证券、银行等板块兴起,三大

闹心!“四车道”成单行,小区门前道路到底谁来管?_全球快播报

“物业已经不管了,市政部门又不接手,小区门前马路成了公共停车场,高峰期想出个门太困难”。5月5日,家住

动态:静乐县气象局发布地质灾害气象风险黄色预警【Ⅲ级/较重】【2023-05-06】

【地质灾害气象风险预警】2023年5月6日9时50分静乐县自然资源局和静乐县气象局联合发布地质灾害气象风险黄

环球热资讯!乘联会:特斯拉中国4月销售75842辆国产汽车 环比减少14.66%

5月6日消息,据外媒报道,乘联会公布的数据显示,今年4月份,特斯拉中国售出了75842辆国产汽车,与去年同期

黑龙江鹤岗市萝北县发生3.1级地震,震源深度8公里

速报参数速报参数:据中国地震台网正式测定,5月6日10时19分在黑龙江鹤岗市萝北县发生3 1级地震,震源深度8

当前简讯:怎么查询医保账户明细?原来查询方法有很多种

怎么查询医保账户明细?原来查询方法有很多种社保网小编为您整理了以下最新资讯供您参考。一、怎么查询医保

董明珠1150万股被冻结?格力回应:录入错误!

针对备受关注的董明珠股份被冻结一事,中新社国是直通车记者以投资者身份致电格力电器董事会秘书办公室,其

世界热资讯!十大大美之城,南京上榜!

2022-2023年度“中国美好生活城市”榜单在成都重磅揭晓南京入选“十大大美之城”!“中国美好生活城市”发

多家A股公司发布股价异动公告 澄清自身业务与大模型、AI关系 快看点

过度联想或为股价异动原因世纪天鸿公告表示,近期市场对人工智能产业等热点保持较高关注度,相关公司股票近

国家二级保护野生动物栗喉蜂虎“组团”飞抵厦门

人民网陈博摄保护区内的岩壁上已有不少栗喉蜂虎筑巢安家

土耳其旅游要多少钱一个月 土耳其旅游要多少钱_世界看热讯

今天来聊聊关于土耳其旅游要多少钱一个月,土耳其旅游要多少钱的文章,现在就为大家来简单介绍下土耳其旅游

每日速递:REITs二级市场价格回调,长期投资价值显现?投资者可关注这些方面

今年2月以来,REITs二级市场出现调整。截至4月28日,有5只产品跌破发行价,年初至今27只产品平均跌幅为3 68%。

猜您喜欢

    Copyright ©  2015-2022 东方服装网版权所有  备案号:沪ICP备2020036824号-8   联系邮箱:562 66 29@qq.com