1. 介绍

python自带的一个单元测试框架。

具备完整的测试结构,支持自动化测试的执行,对测试用例进行组织,并提供了丰富的断言方法,最后会生成测试报告。

2. 核心概念

2.1 测试脚手架

前置 测试 后置

test fixture:环境的准备以及关联的清理动作。setUp()------前置条件、tearDown()-----后置条件,用于初始化测试用例及清理和释放资源。(setUpClass()/tearDownClass())

  1. setUp():每个测试方法运行前运行,测试前置的初始化工作。
  2. tearDown():每个测试方法结束后运行,测试后的清理工作。
  3. setUpClass():所有的测试方法运行前运行,单元测试类运行前的准备工作。必须使用@classmethod装饰器进行装饰。整个测试类运行过程中只会执行一次。
  4. tearDownClass():所有的测试方法结束后运行,单元测试类运行后的清理工作。必须使用@classmethod装饰器进行装饰。整个测试类运行过程中只会执行一次。

2.2 测试用例

TestCase:是最小的测试单元,用于检测特定的输入集合的特定的返回值。unittest提供了TestCase基类,我们创建的测试类需要继承该基类,它可以用来创建新的测试用例。

测试用例,通过继承unittest.TestCase,来实现用例的继承,Unitest中,测试用例都是通过test来识别的。

 def test_xxxx(self):
 
 def xxxx_test(self):

2.3 测试套件

  1. 直接通过unittest.main()方法加载单元测试的测试模块,这是一种最简单的加载方法。所有的测试用例的执行顺序都是按照方法名的字符串表示的ASCII码升序排序。
  2. 将所有的单元测试用例TestCase加载到测试套件Test Suite集合中,然后一次性加载所有测试对象。

test suit是一系列的测试用例

测试套件是测试用例,测试套件或两者的集合。用于组装一组要运行的测试。

测试套件,也称为测试用例集,它用于归档需要一起执行的测试。

Test Suite

if __name__ == '__main__':
    # 创建测试套件
    suit = unittest.TestSuite()
    suit.addTest(TestCalculator("test_add"))
    suit.addTest(TestCalculator("test_sub"))
    suit.addTest(TestCalculator("test_mul"))
    suit.addTest(TestCalculator("test_div"))
 
    # 创建测试运行器
    runner = unittest.TextTestRunner()
    runner.run(suit)
  """
测试用例的执行抛弃了unittest提供的main()方法,而是调用TestSuite类下面的addTest来添加参数用例,因为一次只能添加一条用例,所以需要指定测试类与测试方法。然后,再调用TextTestRunner类下面的run()运行测套件。
  """

Test Loader

该类根据各种标准加载测试用例,并将它们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest提供了可以共享的defaultTestLoader类,可以使用其子类和方法创建实例,discover()就是其中之一。

import unittest
 
test_dir = './test_case'
discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')
 
if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(discover)

discover(start_dir,pattern='test*.py',top_level_dir=None)

找到指定目录下所有测试模块,并可递归查到子目录下的测试模块,只有匹配到文件名才能被加载。如果启动的不是顶层目录,那么顶层目录必须单独指定。

  • start_dir:要测试的模块名或测试用例目录
  • pattern='test*.py':表示用例文件名的匹配原则。此处匹配文件名以“test”开头的“.py”类型的文件,幸好“*”表示任意多个字符
  • top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,默认为None

2.4 测试运行器

test runner 一个用于执行和输出测试结果的组件。

TextTestRunner()

# 创建测试运行器
if __name__ == '__main__':
    ts = unittest.defaultTestLoader.discover('./') # 表示收集当前目录下所有用例
    runner = unittest.TextTestRunner()
    runner.run(ts)

beautifulreport

import unittest
from BeautifulReport import BeautifulReport
# beautifulreport
if __name__ == '__main__':
    
    ts = unittest.defaultTestLoader.discover('./') # 表示收集当前目录下所有用例
    runner = BeautifulReport(ts)
    runner.report("py45期第一份测试报告", 'bfreport.html')

unittestreport

import unittest
import unittestreport


if __name__ == '__main__':
    discover = unittest.defaultTestLoader.discover('./')  # 表示收集当前目录下所有用例
    runner = unittestreport.TestRunner(discover, title='py45期第一份测试报告', desc='木森老师的测试报告模板', tester='shisuiyi')
    runner.run()

3. 单元测试案例

3.1 测试目标函数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/8 21:08
# @Author  : shisuiyi
# @File    : testcode.py
# @Software: win10 Tensorflow1.13.1 python3.9
def login(username, password):
    """
    登录校验的函数
    :param username: 账号
    :param password: 密码
    :return:
    """
    if 6 <= len(password) <= 18:
        if username == 'python45' and password == 'lemonban':
            return {"code": 0, "msg": "登录成功"}
        else:
            return {"code": 1, "msg": "账号或密码不正确"}
    else:
        return {"code": 1, "msg": "密码长度在6-18位之间"}

3.2 设计用例

根据函数的参数和逻辑,设计如下用例:

序号 标题 测试数据 预期结果 实际结果
1 账号密码正确 {"username":"python45", "password":"lemonban"} {"code": 0, "msg": "登录成功"}
2 账号正确密码不正确 {"username":"python45", "password":"lemonban123"} {"code": 1, "msg": "账号或密码不正确"}
3 账号错误密码正确 {"username":"python45123", "password":"lemonban"} {"code": 1, "msg": "账号或密码不正确"}
4 账号正确,密码长度小于6 {"username":"python45", "password":"lemo"} {"code": 1, "msg": "密码长度在6-18位之间"}
5 账号正确,密码长度大于18位 {"username":"python45", "password":"lemonbanlemonbanlemonbanlemonbanlemonban"} {"code": 1, "msg": "密码长度在6-18位之间"}

3.3.编写测试用例并运行

编写步骤:

  1. 导入unittest模块,被检测函数或者类

  2. 创建测试类,继承unittest.TestCase

  3. 如果有初始化条件和结束条件需要编写脚手架代码

  4. 定义测试函数,函数名一定要以test开头

  5. 调用unittest.main()方法运行测试用例

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # @Time    : 2021/11/8 21:08
    # @Author  : shisuiyi
    # @File    : test_login.py
    # @Software: win10 Tensorflow1.13.1 python3.9
    import unittest
    from testcode import login
    
    
    class TestLogin(unittest.TestCase):
        """
        测试登录函数
        """
    
        @classmethod
        def setUpClass(cls) -> None:
            """
            类级前置
            :return:
            """
            print('我是类级前置,在整个类的测试前执行')
    
        @classmethod
        def tearDownClass(cls) -> None:
            """
            类级后置
            :return:
            """
            print('我是类级后置,在整个测试类测试完成后执行')
    
        def setUp(self) -> None:
            """
            方法级前置
            :return:
            """
            print('======我会在每个测试执行前执行====')
    
        def tearDown(self) -> None:
            """
            方法级后置
            :return:
            """
            print('======我会在每个测试执行后执行======')
    
        def test_login_01ok(self):
            """
                测试登录成功
                :return:
                """
            print('1测试登录成功开始测试')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemonban"}
            expect_data = {"code": 0, "msg": "登录成功"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_02password_error(self):
            """
                测试账号正确,密码错误
                :return:
                """
            print('2测试账号正确,密码错误')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemonban123"}
            expect_data = {"code": 0, "msg": "登录成功"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_03username_error(self):
            """
                测试账号错误,密码正确
                :return:
                """
            print('3测试账号错误,密码正确')
            # 1. 准备测试数据
            test_data = {"username": "python45123", "password": "lemonban"}
            expect_data = {"code": 1, "msg": "账号或密码不正确"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_04short_password(self):
            """
                测试账号正确,密码长度过短
                :return:
                """
            print('4测试账号正确,密码长度过短')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemo"}
            expect_data = {"code": 1, "msg": "密码长度在6-18位之间"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_05long_password(self):
            """
                测试账号正确,密码长得过长
                :return:
                """
            print('5 测试账号正确,密码长得过长')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemonbanlemonbanlemonbanlemonbanlemonban"}
            expect_data = {"code": 1, "msg": "密码长度在6-18位之间"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
    
    if __name__ == '__main__':
        unittest.main()
    
    
    # 输出:
    C:\Users\12446\AppData\Local\Programs\Python\Python39\python.exe C:\Users\12446\AppData\Local\JetBrains\Toolbox\apps\PyCharm-C\ch-0\212.5457.59\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py --path D:/Lemon/py45/day16/test_login.py
    Testing started at 21:16 ...
    Launching unittests with arguments python -m unittest D:/Lemon/py45/day16/test_login.py in D:\Lemon\py45\day16
    
    我是类级前置,在整个类的测试前执行
    ======我会在每个测试执行前执行====
    1测试登录成功开始测试
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    2测试账号正确,密码错误
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    3测试账号错误,密码正确
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    4测试账号正确,密码长度过短
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    5 测试账号正确,密码长得过长
    ======我会在每个测试执行后执行======
    我是类级后置,在整个测试类测试完成后执行
    
    
    Ran 5 tests in 0.022s
    
    OK
    
    Process finished with exit code 0
    
    

3.4 测试脚手架

unittest的脚手架有4个?

  • setUp 在每个单元测试开始之前执行
  • tearDown在每个单元测试结束之后执行
  • setUpClass 在整个测试用例类开始前执行
  • tearDownClass 在整个测试用例类测试结束之后执行

3.5 单元测试的执行顺序

按照单元测试函数名的ASCII编码来排序。

3.6 断言方法

方法 检查
assertEqual(a,b) a == b
assertNotEqual(a,b) a!=b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a,b) a is b
assertIsNot(a,b) a is not b
assertIn(a,b) a in b
assertNotIn(a,b) a not in b

4. 测试套件

测试套件就是收集所有的测试用例,然后集中执行。

4.1 通过TestSuite对象收集

这个麻烦,实际不用

4.2 通过TestLoader对象收集

unittest.defaultTestLoader.discover()

discover方法可以根据给定的目录,去自动查找目录下所有符合要求的测试用例,它接受三个参数:

  • start_dir 需要查找的目录
  • pattern 查找模块名的规则,保持默认就好
  • top_level_dir 项目的顶层目录,保持默认就好

注意了:pattern=test*.py参数的规则,需要收的模块名必须以test开头

5. 执行用例&生成报告

直接输出的报告比较丑陋,比如TextTestRunner(unittest自带的)

  • HTMLTestRunnerNew 未知作者,更新什么的不靠谱,不用了

  • BeautifulReport 通过pip可以安装

    • pip install beautifulreport
  • unittestreport 零檬班-木森老师开发的

    • pip install unittestreport
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/9 20:14
# @Author  : shisuiyi
# @File    : main.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
"""
项目的入口文件
"""
import unittest

import unittestreport

from BeautifulReport import BeautifulReport

from test_login import TestLogin

if __name__ == '__main__':
    # # 1. 实例化一个测试套件对象
    # ts = unittest.TestSuite()
    # # 一个一个的添加单元测试
    # # 语法是测试用例类('单元测试方法')
    # ts.addTest(TestLogin('test_login_01ok'))
    # ts.addTest(TestLogin('test_login_02password_error'))
    # # 多个添加
    # ts.addTests([TestLogin('test_login_03username_error'),
    #              TestLogin('test_login_04short_password'),])
    # 运行
    # testloader
    ts = unittest.defaultTestLoader.discover('./') # 表示收集当前目录下所有用例
    # runner = unittest.TextTestRunner()
    # # 传入套件
    # runner.run(ts)

    # beautifulreport
    # br = BeautifulReport(ts)
    # br.report("py45期第一份测试报告", 'bfreport.html')

    # unittestreport
  runner = unittestreport.TestRunner(discover, title='py45期第一份测试报告', desc='木森老师的测试报告模板', tester='shisuiyi')
    runner.run()

6.忽略某个测试方法

import sys
import unittest


class Test001(unittest.TestCase):
    @unittest.expectedFailure  # 即使报错了,也会被计为成功的用例
    def test_1(self):
        print("1+1...", 2)
        assert 1 + 1 == 3

    @unittest.skip('无条件的忽略')  # 不管什么情况都会进行忽略
    def test_2(self):
        print("2+2...", 4)

    @unittest.skipIf(sys.platform == "win32", "跳过")  # 如果系统平台为window则忽略
    def test_3(self):
        print("3+3...", 6)

    @unittest.skipUnless(sys.platform == "win32", "跳过")  # 跳过该用例,除非系统平台为window
    def test_4(self):
        print("4+4...", 8)

    def test_5(self):
        print("5+5...", 10)


if __name__ == "__main__":
    unittest.main(verbosity=2)

执行结果:

test_1 (__main__.Test001) ... 1+1... 2
expected failure
test_2 (__main__.Test001) ... skipped '无条件的忽略'
test_3 (__main__.Test001) ... skipped '跳过'
test_4 (__main__.Test001) ... 4+4... 8
ok
test_5 (__main__.Test001) ... 5+5... 10
ok

----------------------------------------------------------------------
Ran 5 tests in 0.005s

OK (skipped=2, expected failures=1)

可以看到skip有两个分别是test_2和test_3,因为他们的忽略条件都成立了。