三十九、Django单元测试:unittest、测试用例、断言方法总结

为什么编写单元测试

单元测试是软件工程中降低开发成本,提高软件质量常用方式之一,单元测试是一项由开发人员或者测试人员来对程序模块的正确性进行检验测试的工作,用于检查被测试代码的功能是否正确,养成单元测试的习惯,不但可以提高代码的质量,还可以提升自己的编程和技巧。Django 单元测试使用的是 Python 标准库 unittest 模块来定义相应的测试用例。

注意:后续技术分享,第一时间更新,以及更多更及时的技术资讯和学习技术资料,将在公众号CTO Plus发布,欢迎关注公众号:CTO Plus

关于Articulate“做一个知识和技术的搬运工。做一个终身学习的爱好者。做一个有深度和广度的技术圈。”一直以来都想把专业领域的技https://mp.weixin.qq.com/s/0yqGBPbOI6QxHqK17WxU8Q三十九、Django单元测试:unittest、测试用例、断言方法总结_第1张图片

 

单元测试其实就是对模块、类、函数实现的功能执行检测,看看是否满足预期,是否达到功能要求,它是一次检查检验的过程。如果某个模块或者函数满足预期,则表示测试通过,否则表示失败。

1) 降低开发成本

单元测试可以快速的提供反馈,将问题在开发阶段就暴露出来。这样就可以减少向下游传递的问题,尽量避免在系统集成阶段出现了问题。

有数据表明尽早地对软件产品进行测试,将使效率和质量都得到最好的保证。Bug 发现的越晚,修改它所需的费用就越高,因此从费用成本角度来看,应该尽可能早的查找和修改 Bug。在修改费用变的过高之前,单元测试是一种在早期抓住 Bug 的有效途径。

2) 边界检测提高代码质量

边界值检测方法是单元测试中常用的方法之一,比如 1

3) 提高开发人员职业素养

由于互联网的发展,促使了软件行业的发展,企业在遵守行业标椎的同时也制定相关的企业标准使软件开发集成上线更加精细化,形成了一套完成的流程。对于部分开发者来说一旦编码完成,他们总是会迫切希望进行软件的集成工作,这样就能够看到系统的启动了。这在外表上看来确实是明显的进步,但实际而言只是为了进度而进步,严重忽略了单元测试的重要性,若出现问题只会浪费更多的时间成本。

所以编写单元测试,不仅可以提升企业的效率,而且可以使开发人员站在用户的角度重新调整自己的代码,进行代码重构,代码的优化等,这也培养了开发人员的职业素养,提升的他们的编码能力,对于企业的长远发展是非常有利的。

单元测试对象有哪些

1) 核心的业务层代码

对于项目中最核心的部分也就是业务层代码,这是单元测试不可忽视,且非常重要的一项,所以开发人员要保证不会错误。

2) 需要经常修改的代码

对于产品迭代或代码重构而言,可能有些模块需要经常被修改,它们也需要被单元测试所覆盖,这样可以检验修改后的代码是否存在问题。

3) 逻辑复杂的代码

涉及到逻辑层代码,也需要开发人员进行单元测试,做到零纰漏。尤其对于那些至关重要的逻辑代码,需要使用单元测试验证它的可用性,易用性。

unittest模块实现单元测试

Django 的单元测试是基于 Python 的标准库模块 unittest 实现的。在单元测试过程中必须使用断言。unittest 单元测试框架中的 TestCase 类提供了很多断言方法,便于检验测试是否满足预期结果,并能在断言失败后抛出失败的原因。

断言(assertion)是一种在程序中的逻辑(如:一个结果为真或假的逻辑判断式),目的为了表示与验证软件开发者预期的结果——当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息。

1. 编写测试用例代码

1) 编写待测功能函数/模型

待测函数

def select_case():
    calculate = {
        "+": lambda num1, num2: num1 + num2,
        "-": lambda num1, num2: num1 - num2,
        "*": lambda num1, num2: num1 * num2,
        "/": lambda num1, num2: num1 / num2,
        "//": lambda num1, num2: num1 // num2,
        "%": lambda num1, num2: num1 % num2,
    }
    return calculate

需要从 unittest.TestCase 继承。

注意命名规则:unittest 规定只有以 test 开头的方法才是测试方法。

from unittest import TestCase
from packages.drf_pro.utils import select_case

# 定义测试类,继承TestCase
class TestSelectCase(TestCase):
    """
    每一个继承 TestCase 类的子类里面实现的具体的方法(以 test 开头的方法)都是一条测试用例。
    """
    def test_exception(self):
        with self.assertRaises(TypeError):
            self.assertEqual(0, select_case()['/'](0, 1))

    def test_calc(self):
        self.assertEqual(33, select_case()["+"](11, 22))
        self.assertNotEqual(select_case()["-"](11, 22), 0)
        self.assertTrue(select_case()["*"](1.1, 1))
        self.assertFalse(select_case()["*"](1.1, 0))

2. 执行单元测试的方法

  • pycharm执行

测试类或测试函数旁边出现绿色的可运行图标,点击Run或者Debug,运行测试

三十九、Django单元测试:unittest、测试用例、断言方法总结_第2张图片

三十九、Django单元测试:unittest、测试用例、断言方法总结_第3张图片

  • 使用unittest单元测试模块提供的方法执行

python -m unittest test_case1

  • 执行 TestScore 类中定义的测试用例

python -m unittest test_case1.TestSelectCase.test_calc

Django项目编写单元测试用例

Django中的单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

Python中最常用的单元测试方法是unittest.TestCase,而Django中的django.test.Testcase是在前者的基础上进一步的封装。Django的设计模式是MTV,所以主要测试的是模型跟视图,也就是数据库模型跟代码逻辑。Django中有完善的单元测试,可以对开发的每一个功能进行单元测试,就可以测试功能是否正常。

当使用 startapp 命令创建 app 应用的时候,app目录下就会有一个 tests.py 文件,这个文件就是 Django 提供给开发者做单元测试的,在这个文件中给出了测试类需要继承的基类TestCase,其中django.test.TestCase是 unittest.TestCase 的一个子类,实现了数据库访问以及 HTTP 请求等测试功能。

三十九、Django单元测试:unittest、测试用例、断言方法总结_第4张图片

类继承关系图

项目而言,因为它的模块众多,所以在做测试的时候为了更加直观、方便,我们通常在应用下创建一个测试目录用来承载不同模块的测试用例,按照模块的类别为不同的功能函数定义测试代码,如 test_models.py、test_vierws.py 分别指的是模层与视图层,以这样的命名方式来对文件命名。

1) 基础功能测试

app/tests.py

from django.test import TestCase

# 继承Django中的测试模块 即unittest.TestCase的子类django.test.TestCase
class TestExample(TestCase):
    def test_case(self):
        def calculate(num1, num2):
            return sum([num1, num2]), num1 + num2
        self.assertEqual(calculate(11,22)[0], 33)

执行单元测试命令:

python manager.py test packages.drf_pro.tests.TestExample.test_case --keepdb

2) 模型测试

app/tests_models.py

from django.test import TestCase
from .models import Config

class TestModels(TestCase):
    """
    模型测试:对 Model 的增删改查进行测试,测试类继承 django.test.TestCase。
    在执行测试用例之前创建test_开头的测试数据库,并在执行测试用例之后销毁。
    """
    def test_config_modles(self):
        conf1 = Config.objects.create(name_zh = "配置1", name_en = "config1", conf_value = "config value1", remark = "配置1内容",conf_type = "0")
        self.assertIsNotNone(conf1)
        self.assertTrue(conf1 is not None)

        Config.objects.create(name_zh = "配置2", name_en = "config2", remark = "配置3内容", conf_type = "2")
        Config.objects.create(name_zh = "配置3", name_en = "config3", conf_value = "config value3", remark = "配置3内容")

        count = Config.objects.count()
        self.assertEqual(count, 3)
        self.assertNotEqual(count, 1)

3) 视图层测试

app/tests_views.py

from django.conf import settings
from django.test import TestCase


class TestViews(TestCase):
    """
    视图层测试:引入测试客户端,它提供了 get、post等方法实现了对视图的访问,测试客户端被封装在了django.test.Client下。
    测试类的每一个测试方法都可以直接使用测试客户端 self.client。
    每一个测试方法都会新建一个测试客户端,并且彼此之间互不影响。
    """

    def test_index_view(self):
        # GET请求 对相应视图函数的访问
        response = self.client.get("http://127.0.0.1:81/drf_pro/index/")
        # POST请求
        # self.client.post()
        response["steverocket-token"] = settings.SECRET_KEY

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response["steverocket-token"], 'django-insecure-!hh4oj&-(8xp$d-17!bzmx+ilgkccym32ovkx7hr4f#9cs5y-6')

执行测试用例命令

执行项目下的所有测试用例并输出

python manage.py test

执行 index 应用下的所有测试用例

python manage.py test index

执行 index应用下 tests 模块下定义的测试用例

python manage.py test.index.tests

直接执行 tests.py 文件下测试类

python manage.py test index.tests.ExampleTest

直接执行测试类下某个测试方法

python manage.py test index.tests.ExampleTest.test_view

执行测试用例,如果不希望重复创建测试库可以添加—keepdb参数

python manage.py test index --keepdb

加上 -v 参数来更加详细的显示测试过程,不仅如此可以使用数字 0、1、2、3 来指定详细程度,数字越大表示输出越详细,只有这 4 个级别可选则

python manage.py test -v 1 index.tests

断言方法

unittest提供的断言方法

三十九、Django单元测试:unittest、测试用例、断言方法总结_第5张图片

  • assertEqual(first,second,msg=None)

测试first == second,否则抛出断言异常信息msg

示例:self.assertEqual(13, select_case()["+"](11, 22), "相加计算结果不相等")

三十九、Django单元测试:unittest、测试用例、断言方法总结_第6张图片

  • assertNotEqual(first,second,msg = None)

测试first!=second,否则抛异常信息msg

  • assertTrue(expr,msg=None)

测试表达式expr为True,否则抛出断言异常信息msg

  • assertFalse(expr,msg=None)

测试表达式expr为False,否则抛出断言异常信息msg

  • assertIs(a,b,msg=None)

测试a和b是同一对象,否则抛出断言异常信息msg

  • assertIsNot(a,b,msg=None)

测试a和b不是同一对象,否则抛出断言异常信息msg

  • assertIsNone(expr,msg=None)

测试表达式expr结果为None,否则抛出断言异常信息msg

  • assertIsNotNone(expr,msg=None)

测试表达式expr结果不为None,否则抛出断言信息异常

  • assertIn(a,b,msg=None)

测试a包含在b中,否则抛出断言异常信息msg

  • assertIsInstance(obj,cls,msg=None)

断言obj为cls类型,否则抛出断言异常信息msg

  • assertRaisesRegexp(exc,r[,fun,*args,**kwds])

测试函数fun(*args,**kwds)抛出exc异常,同时可以用正则r去匹配异常信息exc,否则抛出断言异常

  • assertRaises(exc[,fun,*args,**kwds])

测试函数fun(*args,**kwds)抛出exc异常,否则抛出断言异常

Django unittest测试框架核心概念

unittest 单元测试框架受到了 JUnit(JUnit 是一个 Java 语言的单元测试框架)的启发,它与其他语言的测试框架有着相似的风格

  1. test fixture:它代表的是初始化和清理测试环境,它最常见的使用场景,比如数据库连接的创建与销毁。
  2. test case:它代表的是 unittest.TestCase 类实例,一个完整的测试单元,通过运行这个测试单元实现最终的测试验证。
  3. test suite:它代表的是 test case 的集合,同时 test suite 之间可以进行嵌套,从而达到多个测试任务一起执行的目的。
  4. test runner:它代表的是运行测试用例,然后给用户最终的测试结果。

1) 初始化和清理测试环境

初始化和清理测试环境是使用 unittest 框架进行测试第一步也是最重要的一步,这个过程中涉及到了两个方法即 setUp 和 tearDown,它们分别负责初始化工作和环境的清理工作,它们在每次执行测试用例的前后执行,即 setUp 在执行测试用例前执行处理化工作,tearDown 在测试用例执行完成后做一些清理的工作。

class TestSetUpTearDown(TestCase):
    def setUp(self) -> None:
        logger.info("start init.....")
        Config.objects.create(name_zh="配置1", name_en="config1", conf_value="config value1", remark="配置1内容", conf_type="0")
        Config.objects.create(name_zh="配置2", name_en="config2", remark="配置3内容", conf_type="2")
        Config.objects.create(name_zh="配置3", name_en="config3", conf_value="config value3", remark="配置3内容")
        logger.info("end init.....")

    def tearDown(self) -> None:
        logger.warning(f"清空配置表数据.....[{Config.objects.count()}]")
        Config.objects.filter().delete()
        logger.warning(f"清空配置表数据.....[{Config.objects.count()}]")

    def test_models(self):
        self.assertIsNotNone(Config.objects.filter(name_en="config1"))
        self.assertEqual(3, Config.objects.count())

    def test_views(self):
        response = self.client.get("http://127.0.0.1:81/drf_pro/index/")
        self.assertEqual(response.status_code, 200)

执行结果,每个测试用例执行的前后都执行了 setUp 和 tearDown 方法。

装饰器实现跳过测试与预期失败

unittest 框架还有一个重要特性就是可以跳过某些测试用例或者已经预期失败的用例。跳过测试用例很好理解,就是不执行某些测试方法,甚至可以直接跳过测试类,那么预期失败怎么解释呢,其实也很好理解,就是已经预测到因为某些原因导致的测试不通过,但是在执行测试的时候不希望该条测试用例仍然标记为失败,那么在这个时候,我们就可以想办法跳过。

1) 跳过测试装饰器

跳过测试的功能可以使用装饰器实现,这类装饰器有以下三个:

  1. unittest.skip(reason):无条件跳过,其中 reason 用来表示跳过测试的原因;
  2. unittest.skipIf(condition,reason):当条件(condition)成立的时候,跳过测试;
  3. unittestUnless(condition,reason):与 skipIf 相反,当条件(condition)不成立的时候,跳过测试。
import sys
import unittest
from django.test import TestCase

class TestSetUpTearDownSkip(TestCase):
    def setUp(self) -> None:
        """
        把需要初始化的工作放在 setUp 中执行
        """
        self.version = sys.version
        print(self.version)

    def tearDown(self) -> None:
        print("单元测试结束,清理测试数据.......")

    def test_case(self):
        self.assertIsNotNone(self.version)

    @unittest.skip("不测试test1")
    def test1(self):
        print("test1.....")

    @unittest.skipIf(True, "跳过test2")
    def test2(self):
        print("test2.....")

    @unittest.skipUnless(False, "跳过test3")
    def test3(self):
        print("test3.....")

三十九、Django单元测试:unittest、测试用例、断言方法总结_第7张图片

如果想直接跳过测试类,可以直接在类名上方使用装饰器即可

2) 跳过预期失败装饰器

使用unittest.expectedFailure处理预期失败的用例,不管是标注了该装饰器的方法可以通过测试,还是标注了该装饰器的类中有通过测试的方法,它们都会被认为是测试失败即 FAILED,它提供了两个参数,如下所示:

FAILED (expected failures=1, unexpected successes=1)

expected failures=1 表示使用改装饰器的方法确实测试不通过;

unexpected successes=1 表示该方法中某些断言可以测试通过,但并不代表所有断言都通过测试。

参考资料

  1. Django单元测试:编写并运行测试 | Django 文档 | Django
  2. https://docs.python.org/zh-cn/3.7/library/unittest.html
  3. Writing your first Django app, part 5 | Django documentation | Django
  4. unittest — Unit testing framework — Python 3.11.2 documentation


更多资料 · 微信公众号搜索【CTO Plus】关注后,获取更多,我们一起学习交流。

关于公众号的描述访问如下链接


关于Articulate“做一个知识和技术的搬运工。做一个终身学习的爱好者。做一个有深度和广度的技术圈。”一直以来都想把专业领域的技https://mp.weixin.qq.com/s/0yqGBPbOI6QxHqK17WxU8Q

你可能感兴趣的:(#,Django进阶,单元测试,django,测试用例)