Django笔记三十六之单元测试汇总介绍

本文首发于公众号: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 的数据库。

如果想获取更多后端相关文章,可扫码关注阅读:
image

原文链接:https://www.cnblogs.com/hunterxiong/p/17378703.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Django笔记三十六之单元测试汇总介绍 - Python技术站

(0)
上一篇 2023年5月7日
下一篇 2023年5月8日

相关文章

  • Python实现自动批量修改文件名称

    Python实现自动批量修改文件名称攻略 Python可以轻松实现自动批量修改文件名称。如果你有大量的文件需要重命名,手动一个一个修改比较费时间,可以使用Python的os模块进行批处理,完成自动化重命名。 步骤 以下是实现批量文件重命名的基本步骤: 导入os模块,获取文件路径和文件名; 构建新文件名; 在循环中将旧文件名修改为新文件名。 示例 下面两个示例…

    python 2023年5月19日
    00
  • Python+Selenium自动化环境搭建与操作基础详解

    下面就来详细讲解“Python+Selenium自动化环境搭建与操作基础详解”的完整攻略: 环境搭建 安装Python 首先需要安装Python,建议使用Python3版本以上。可在官网下载安装包进行安装,也可通过命令行工具安装。 sudo apt-get install python3 安装浏览器驱动 由于Selenium是通过模拟浏览器操作实现自动化测试…

    python 2023年5月19日
    00
  • Python实现简单的图书管理系统

    下面是Python实现简单的图书管理系统的完整攻略: 一、需求分析 在开始编写代码之前,我们需要先明确该系统的功能需求。根据常规图书管理系统的特点,我们可以归纳出以下几个需求: 管理员可以登录系统,通过普通用户的注册与管理维护用户信息。 管理员可以添加、删除、修改、查询图书信息。 普通用户可以借阅并查询图书信息。 综上所述,我们需要实现如下四个功能: 用户管…

    python 2023年5月19日
    00
  • python实现语音常用度量方法的代码详解

    Python实现语音常用度量方法的代码详解 语音信号处理是一项重要的研究领域,其中常用的度量方法包信噪比(SNR)、语音质量评估(PESQ)和语音识别率(WER)等。在本攻略中,我们将介绍如何使用Python实现这些常用的度量方法,并提供两个示例来说明如何使用这些度量方法进行语音信号处理。 步骤1:了解常用的度量方法 在语音信号处理中,常用的度量方法包括: …

    python 2023年5月14日
    00
  • python PyQt5 爬虫实现代码

    下面是关于python PyQt5爬虫实现代码的详细攻略。 标准步骤 实现爬虫代码一般分为以下几个步骤:1. 确定爬取网站的 URL2. 获取HTML源代码3. 解析HTML源代码,提取所需信息4. 存储爬取到的数据 在使用Python编写爬虫代码时,需要使用一些第三方库来协助完成上述步骤,比如爬取网站的URL可以使用requests库,获取HTML源代码则…

    python 2023年5月14日
    00
  • 详解python3实现的web端json通信协议

    当今Web应用的需求越来越复杂,跨平台跨语言通信的需求也越来越高。JSON已经成为一种流行的数据交换格式,它轻量级且易于阅读和编写,能够方便地进行各种语言之间的数据传输。本攻略演示如何在Python3中实现Web端JSON通信协议。 步骤一:环境准备 安装Python3.x 安装Flask框架:pip install Flask 步骤二:实现JSON通信 使…

    python 2023年6月3日
    00
  • python中pygame安装过程(超级详细)

    下面我将详细讲解Python中Pygame安装过程的攻略。 Pygame安装过程 1. 安装Python 在进行Pygame安装之前,首先需要安装Python。可以前往Python官网(https://www.python.org/)下载Python的安装包,选择适合自己的操作系统版本进行下载。下载完成后,按照安装向导进行安装。 2. 安装Pygame依赖 …

    python 2023年5月14日
    00
  • 关于Python的高级数据结构与算法

    下面是关于“Python的高级数据结构与算法”的完整攻略。 1. 高级数据结构 1.1 堆 堆是一种特殊的树形数据结构,它满足堆的性质对于每个节点x,它的父节点的值小于等于x的值。在Python中,我们可以使用heapq模块来实现。 import heapq # 创建一个堆 my_heap = [] heapq.heappush(my_heap, 3) he…

    python 2023年5月13日
    00
合作推广
合作推广
分享本页
返回顶部