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

yizhihongxing

本文首发于公众号: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 re.sub 反向引用的实现

    Python中的re.sub函数可以用于对字符串内容进行替换操作,而在替换过程中,反向引用是其一个非常有用的功能。本文将详细讲解Python re.sub反向引用的实现攻略。 什么是反向引用? 反向引用指的是在正则表达式的替换操作中,可以使用捕获组的内容作为替换的一部分,通过在替换字符串中添加类似’\g<组号>’的格式,就可以实现对捕获组内容的引…

    python 2023年6月3日
    00
  • 深入讲解Python中面向对象编程的相关知识

    深入讲解Python中面向对象编程的相关知识 面向对象编程是一种流行的程序设计范式,其核心思想是将程序中的对象抽象出来,然后定义它们的属性和方法,从而实现代码的复用和模块化。Python作为一种面向对象的编程语言,具有强大的面向对象特性,让程序员能够更高效地编写和管理复杂的程序。 什么是面向对象编程 在面向对象编程中,一个对象是一个具有状态和行为的实体。例如…

    python 2023年5月30日
    00
  • Python基于Socket实现简易多人聊天室的示例代码

    下面是详细的攻略。 Python基于Socket实现简易多人聊天室 概述 在本示例中,我们将使用Python的Socket库建立一个简单的多人聊天室。我们将会通过网络实现实时通信,让不同的客户端可以在同一台主机上互相聊天,并且能够观察到其他用户的消息。 实现步骤 1. 创建服务端 在Python中使用Socket实现多人聊天室,需要先创建一个服务端程序,接受…

    python 2023年5月19日
    00
  • 如何理解Python中的变量

    理解Python中的变量是Python编程中的基础知识之一,这里我们将从以下几个方面逐一进行讲解: 什么是变量 变量是程序中存储值的容器,可以将数据存储在变量中,变量可以是数字、字符串、布尔值、对象等。程序中的变量是有类型的,由于Python是一种解释性语言,因此变量声明和类型定义是自动的,无需手动指定类型。 如何声明变量 在Python中声明变量非常简单,…

    python 2023年5月18日
    00
  • Python基于多线程实现抓取数据存入数据库的方法

    在本攻略中,我们将介绍如何使用Python基于多线程实现抓取数据并存入数据库。以下是一个完整攻略,包括两个示例。 步骤1:创建数据库 首先,我们需要创建一个数据库来存储抓取的数据。我们可以使用MySQL数据库,也可以使用其他数据库,如PostgreSQL、SQLite等。 以下是一个示例代码,演示如何使用MySQL数据库创建一个名为“test”的数据库: C…

    python 2023年5月15日
    00
  • Python中转换角度为弧度的radians()方法

    Python的math模块提供了一些用于数学计算的方法和常数,其中就包括了转换角度为弧度的方法radians()。 方法介绍 该方法的作用是将度数转换为弧度,其函数原型为: math.radians(x) 其中,x是待转换的度数。 方法示例 示例1:将30度转换为弧度 import math degrees = 30 radians = math.radia…

    python 2023年6月3日
    00
  • Python写一个字符串数字后缀部分的递增函数

    下面是Python写一个字符串数字后缀部分的递增函数的完整攻略: 1. 分析问题 首先,我们要对问题进行分析,明确需求,才能更好地解决问题。 本问题要求写一个函数,功能是对输入的字符串数字后缀进行递增,例如:将”file_1″转换为”file_2″,将”file_99″转换为”file_100″。需要注意的是,数字后缀的长度是不确定的,可以是1位、2位、3位…

    python 2023年6月5日
    00
  • Python二元算术运算常用方法解析

    下面是详细讲解“Python二元算术运算常用方法解析”的完整攻略。 1. 什么是二元算术运算? 二元算术运算是指对两个数运算的操作,包括加法、减法、乘法、除法等。 2. Python二元算术运算常用方法 2.1 加法运算 加法运算是指将两个数相加的操作,可以使用加号(+)进行运算。 下面是一个加法运算的示例: a = 5 b = 3 c = a + b pr…

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