跳转至

单元测试框架

  1. 测试发现:从多个py文件中收集并加载测试用例
  2. 测试执行:将测试用例按照一定的顺序和条件取执行并生成结果
  3. 测试判断:通过断言取判断结果是否正确
  4. 测试报告:统计测试进度,通过率,生成报告

unittest

1. 重要组件

  1. TestCase
  2. TestSuite
  3. TestFixTure
  4. TestLoader
  5. TestRunner

2. TestCase

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import unittest


class EcshopLogin(unittest.TestCase):
    # 测试用例
    def test01_register(self):
        print("注册测试")

    def test02_login(self):
        print("登录测试")

1. 使用命令行方式运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# python -m unittest 文件名
python -m unittest ecshop_login.py
注册测试
.登录测试
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

# python -m unittest 模块名.类名
python -m unittest ecshop_login.EcshopLogin
注册测试
.登录测试
.
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK
# python -m unittest 模块名.类名.方法名
python -m unittest ecshop_login.EcshopLogin.test02_login
登录测试
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
# python -m unittest -v 模块名.类名.方法名 
# -v 显示详细
python -m unittest -v ecshop_login.EcshopLogin.test02_login
test02_login (ecshop_login.EcshopLogin) ... 登录测试
ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

2. 执行结果分析

1
2
3
4
. 成功
F 失败
E 错误
S 跳过

3. 用例执行顺序

按照ASCII码的规则:0-9 A-Z a-z

4. 使用TestSuite只运行部分用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import unittest


class EcshopLogin(unittest.TestCase):
    # 测试用例
    def test01_register(self):
        print("注册测试")

    def test02_login(self):
        print("登录测试")


if __name__ == '__main__':
    suite = unittest.TestSuite()
    # 添加测试单元方式一
    suite.addTest(EcshopLogin("test02_login"))
    # 添加测试单元方式二
    #testcases = [EcshopLogin("test02_login")]
    #suite.addTests(testcases)
    # 添加测试单元方式三
    #testcases = unittest.defaultTestLoader.discover(start_dir=os.getcwd(), pattern="test*.py")
    #suite.addTests(testcases)


    # 运行方式一
    # unittest.main(defaultTest='suite')
    # 运行方式二
    unittest.TextTestRunner().run(suite)
1
2
3
4
5
6
7
python ecshop_login.py
登录测试
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

5. Testfixture

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import unittest


def setUpModule():
    print("start Module TestFixture")


def tearDownModule():
    print("end Module TestFixture")


class EcshopLogin(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        print("setUpClass: 在每个类之前执行一次")

    def setUp(self) -> None:
        print("setUp: 在每个测试用例之前的准备工作")

    def test01_register(self):
        print("注册测试")

    def test02_login(self):
        print("登录测试")

    def tearDown(self) -> None:
        print("tearDown: 在每个测试用例之后的清理工作")

    @classmethod
    def tearDownClass(cls) -> None:
        print("tearDownClass: 在每个类之后的执行一次")


if __name__ == '__main__':
    unittest.main()

执行结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ python ecshop_login.py
start Module TestFixture
setUpClass: 在每个类之前执行一次
setUp: 在每个测试用例之前的准备工作
注册测试
tearDown: 在每个测试用例之后的清理工作
.setUp: 在每个测试用例之前的准备工作
登录测试
tearDown: 在每个测试用例之后的清理工作
.tearDownClass: 在每个类之后的执行一次
end Module TestFixture

6. 忽略测试用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest

@unittest.skip("此模块忽略测试")
class EcshopLogin(unittest.TestCase):
    age = 20

    @unittest.skip("无条件忽略")
    def test01_register(self):
        print("注册测试")

    @unittest.skipIf(age > 18, "条件为真忽略")
    def test02_login(self):
        print("登录测试")

    @unittest.skipUnless(age > 18, "条件为假忽略")
    def test03_logout(self):
        print("登出测试")


if __name__ == '__main__':
    unittest.main()

7. 断言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import unittest


class EcshopLogin(unittest.TestCase):

    def test01_register(self):
        self.assertTrue([])

    def test02_login(self):
        self.assertEqual(1, 2)


if __name__ == '__main__':
    unittest.main()

8. DDT装饰器

1
2
3
4
@ddt  # 类装饰器,声明当前类使用DDT框架
@data # 函数装饰器,用于给测试用例传递参数
@unpack # 函数装饰器,将传输的数据包解包,一般作用于元组和列表
@file_data # 函数装饰器,可直接读取yaml/json文件

@data的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import unittest

import ddt as ddt


@ddt.ddt()
class LoginTest(unittest.TestCase):
    @ddt.data('tom')  # 传几个值,用例执行几次
    def test01_login(self, name):
        print(name)


if __name__ == '__main__':
    unittest.main()

@unpack的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import unittest

import ddt as ddt


@ddt.ddt()
class LoginTest(unittest.TestCase):
    @ddt.data(("tom", 18), ("jerry", 16))
    @ddt.unpack
    def test01_login(self, name, age):
        print(name, age)


if __name__ == '__main__':
    unittest.main()

9. 源码分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class TestProgram(object):
    def __init__(self, 
                 module='__main__',  # 测试用例所在的路径
                 defaultTest=None,  # 待测试用例的名称,默认所有
                 argv=None,  # 接收外部参数
                 testRunner=None,   # 测试运行器,默认为TextTestRunner
                 testLoader=loader.defaultTestLoader,  # 指定默认测试加载器
                 exit=True,  # 是否在测试完成后结束python程序
                 verbosity=1,  # 类似于命令行-v [0|1|2]
                 failfast=None, 
                 catchbreak=None,
                 buffer=None, 
                 warnings=None, 
                 *, 
                 tb_locals=False):

二、pytest

1. 编写规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pytest


class TestLogin:

    def test_01_login(self):
        print("登录测试")


if __name__ == '__main__':
    pytest.main(['-sv'])

2 执行

1. 执行方式

1. 主函数模式
  1. 执行所有:pytest.main()
  2. 指定模块:pytest.main(['-sv','test_login.py']
  3. 指定目录:pytest.main(['-sv', './testcases_dir'])
  4. 通过nodeid指定:nodeid由模块名,分隔符,类名,方法名,函数名组成pytest.main(['-sv', 'test_login.py::TestLogin::test_01_login'])
2.命令行模式
  1. 执行所有:pytest
  2. 指定模块:pytest -sv test_login.py
  3. 指定目录:pytest -sv ./testcases_dir
  4. 通过nodeid指定:nodeid由模块名,分隔符,类名,方法名,函数名组成pytest -sv test_login.py::TestLogin::test_01 _login

2. 常用参数

1
2
3
-s: 表示输出调试信息,包括print打印的信息
-v: 显示更详细的信息
-n 分布式运行

单元测试框架对比

1. 用例编写规则

unittest: 提供了testcase(测试用例)、testsuites(测试套件)、testfixtures(测试固件/夹具)、testloader(测试加载器)、testrunner(测试运行器)。必须遵守以下规则:

  1. 测试文件必须导入:import unittest
  2. 测试类必须继承unittest.TestCase
  3. 测试方法必须以test开头

pytest:第三方测试框架,基于unittest的扩展框架,必须遵守以下规则:

  1. 测试文件名必须以test_开头或者_test结尾
  2. 测试类必须以Test开头
  3. 测试方法必须以test开头

2. 用例的前置和后置

unittest:

  1. setUp/tearDown 在每个用例之前/之后运行一次。如打开浏览器,加载/关闭网页
  2. setUpClass/teatDownClass 在每个类运行之前/之后运行一次。如创建/关闭数据库连接,创建/销毁日志对象
  3. setUpModule/tearDownModule 在每个模块之前/之后执行一次

pytest:

  1. 方法级
    1. setup_method/teardown_method 在方法之前/之后。优先级高
    2. setup/teardown 在方法之前/之后。优先级低
  2. 函数级
    1. setup_function/teardown_function 在函数之前/之后
  3. 类级
    1. setup_class/teardown_class 在类之前/之后
  4. 模块级
    1. setup_module/teardown_module 在模块之前/之后
  5. 装饰器:可以在函数之前加pytest_fixture()实现上述的功能

3. 断言

unittest: assertTrueassertEqualassertin

pytest: assert

4. 报告

unittest: htmltestrunner

pytest:插件(pytest-HTMLallure

5. 失败重跑

unittest:无

pytest: pytest-rerunfailures插件

6. 数据驱动

unittest: DDT

pytest: @pytest.mark.parametrize装饰器

7. 用例分类执行

unittest:默认执行所有,也可以用过testsuite来执行部分用力,或者-k参数

pytest:@pytest.mark