单元测试本质是通过一段代码去验证另外一段代码。单元测试框架不仅可以用来做单元测试,它还适用于不同类型的“自动化”测试
单元测试框架功能如下:
创建一个计算器类:
# 计算器类
class Calculator(object):
"""用于完成两个数的加、减、乘、除"""
def __init__(self, a, b):
self._a = a
self._b = b
# 加法
def add(self):
return self._a + self._b
# 减法
def sub(self):
return self._a - self._b
# 乘法
def mul(self):
return self._a * self._b
# 除法
def div(self):
return self._a / self._b
传统测试,是在测试代码中,首先引入calculator.py文件中国的Calculator类,并对测试数据进行初始化,接下来调用该类下面的方法,得到结果,使用assert断言是否正确。
from calculator import Calculator
def test_add():
c = Calculator(3, 5)
result = c.add()
assert result == 3
if __name__ == "__main__":
test_add()
但这样的的测试存在着一些问题,首先,需要自己定义断言失败的提示;其次,当一个测试函数运行失败时,后面的测试函数不在执行;最后,执行结果无法统计。
from calculator import Calculator
import unittest
class TestCalculator1(unittest.TestCase):
# 测试加法
def test_add(self):
c = Calculator(3, 5)
result = c.add()
self.assertEqual(result, 8)
# 测试减法
def test_sub(self):
c = Calculator(5, 2)
result = c.sub()
self.assertEqual(result, 4)
# 测试乘法
def test_mul(self):
c = Calculator(3, 4)
result = c.mul()
self.assertEqual(result, 12)
if __name__ == "__main__":
unittest.main()
引入unittest模块。如果想用unittest编写测试用例。必须遵守它的“规则”。
在unittest文档中有四个重要概念:TestCase、Test Suite、Test Runner和Test Fixture
它是最小的测试单元,用于检查特定输入集合的特定返回值。unittest提供了TestCase基类,我们创建的测试类需要继承该基类,他可以用来创建新的测试用例。
测试套件是测试用例、测试套件或两者的集合,用于组装一组要运行的测试。unittest提供了TestSuite类来创建测试套件。
Test Runner是一个组件,用于协调测试的执行并向用户提供结果。Test Runner可以使用图形界面、文本界面或返回特殊值来展示执行测试的结果。unittest提供TextTestRunner类来运行测试用例,为了生成HTML格式的测试报告,可以选择使用第三方运行类(HTMLTestRunner)。
Test Fixture代表执行一个或多个测试所需要的环境准备,以及关联的清理动作,例如,创建临时或代理数据库、目录,或启动服务器进程。unittest中提供了setUp()/tearDown()、setUpClass()/tearDownClass()等方法来完成这些操作。
import unittest
from calculator import Calculator
class TestCalculator2(unittest.TestCase):
# 测试用例前置动作
def setUp(self):
print("Test Start---")
# 测试用例后置动作
def tearDown(self):
print("Test end----")
# 测试加法
def test_add(self):
c = Calculator(3, 5)
result = c.add()
self.assertEqual(result, 8)
# 测试减法
def test_sub(self):
c = Calculator(5, 2)
result = c.sub()
self.assertEqual(result, 4)
# 测试乘法
def test_mul(self):
c = Calculator(3, 4)
result = c.mul()
self.assertEqual(result, 12)
if __name__ == "__main__":
# 创建测试套件
suite = unittest.TestSuite()
suite.addTest(TestCalculator2("test_add"))
suite.addTest(TestCalculator2("test_sub"))
suite.addTest(TestCalculator2("test_mul"))
# 创建测试运行器
runner = unittest.TextTestRunner()
runner.run(suite)
在执行测试用例过程中,最终测试用例执行成功与否,是通过测试得到的实际结果与预期结果进行比较得到的。unittest框架的TestCase类提供的用于测试结果的断言方法如下:
方法 | 检查 | 版本 |
---|---|---|
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 | |
assertIsNone | x is None | |
assertIsNotNone(x) | x is not None | |
assertIn(a, b) | a in b | |
assertNotIn | a not in b | |
assertIsInstance(a, b) | isinstance(a, b) | |
assertNotIsInstance(a, b) | not isinstance(a,b) |
一个功能对应一条测试用例显然是不够的,要写多少测试用例取决于你对功能需求与测试方法的理解。对于测试用例的划分,建议一个测试类对应一个被测试的功能。
import unittest
from calculator import Calculator
class TestAdd(unittest.TestCase):
"""add() 方法测试"""
def test_add_integer(self):
"""整数相加"""
c = Calculator(3, 5)
result = c.add()
self.assertEqual(result, 8)
def test_add_decimal(self):
"""小数相加"""
c = Calculator(3.2, 5.3)
self.assertEqual(c.add(), 8.5)
def test_add_string(self):
"""字符串相加"""
c = Calculator("hello", "world")
self.assertEqual(c.add(), "hello world")
class TestSub(unittest.TestCase):
"""sub() 方法测试类"""
pass
if __name__ == "__main__":
unittest.main()
如何执行多个测试文件?unittest中的TestLoader类提供的discover()方法可以从多个文件中查找测试用例。
该类根据各种标准加载测试用例,并将它们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest提供了可以共享的defaultTestLoader类,可以使用其子类或方法创建实例,discover方法就是其中之一
discover(start_dir, pattern="test*.py", top_level_dir=None)
找到指定目录及其子目录下的所有测试模块,只有匹配的文件名才能被加载。如果启动的不是顶层目录,那梦顶层目录必须单独指定。
之后通过discover方法重新实现run_tests.py文件的功能
import unittest
# 定义测试用例的目录为当前目录
test_dir = "./"
suits = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py")
if __name__ == "__main__":
runner = unittest.TextTestRunner()
runner.run(suits)
unittest默认根据ASCII码的顺序加载测试用例,所以TestAdd类会优先TestBdd类先执行,test_add()方法会优先test_bdd()方法执行。在discover()和main()方法中,也是如此,test_aaa.py文件会优先test_bbb.py执行。并不是按照测试用例的创建顺序从上到下执行的。因此,如果想让某个测试文件先执行,可以在命名上加以控制。
import unittest
class TestAdd(unittest.TestCase):
def setUp(self):
print("test TestAdd")
def test_ccc(self):
print("test ccc")
def test_aaa(self):
print("test aaa")
class TestBdd(unittest.TestCase):
def setUp(self):
print("test TestBdd")
def test_bbb(self):
print("test bbb")
if __name__ == "__main__":
unittest.main()
执行结果
test TestAdd
test aaa
test TestAdd
test ccc
test TestBdd
test bbb
除命名外,还可以声明测试套件TestSuite类,通过addTest()方法按照一定的顺序来加载测试用例
if __name__ == "__main__":
suites = unittest.TestSuite()
suites.addTest(TestBdd("test_bbb"))
suites.addTest(TestAdd("test_aaa"))
suites.addTest(TestAdd("test_ccc"))
runner = unittest.TextTestRunner(verbosity=3)
runner.run(suites)
执行结果
test_bbb (__main__.TestBdd) ... ok
test_aaa (__main__.TestAdd) ... ok
test_ccc (__main__.TestAdd) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
test TestBdd
test bbb
test TestAdd
test aaa
test TestAdd
test ccc
注意
:在pycharm中,引入了unittest模块,会默认按照unittest模式执行。需要将unittest模式转换成普通模式。
unittest模式转换成普通模式的方法:https://blog.csdn.net/qq_33356414/article/details/94556140
当测试用例的数量达到一定量级,需要考虑目录的划分,如下:
对于以上目录结果,如果将discover方法中的start_dir参数定义为【./test_case】目录,那只能加载【test_a.py】文件的测试用例,解决办法是需要在每个目录下放一个【__init__.py
】文件,将一个目录标记成一个标准的Python模块。
运行测试时,有时需要直接跳过某些测试用例,或者当测试用例符合某个条件时跳过测试,又或者直接将测试用例摄者为失败。unittest提供了实现这些需求的装饰器
unittest.skip(reson)
: 无条件跳过装饰的测试,需要说明跳过原因unittest.skipIf(condition, reason)
: 满足条件,则跳过装饰的测试unittest.skipUnless(condition, reason)
: 满足条件,执行装饰的测试unittest.expectedFailure()
: 不管执行结果是否失败,都将测试标记为失败import unittest
class MyTest(unittest.TestCase):
@unittest.skip("直接跳过测试")
def test_skip(self):
print("test skp")
@unittest.skipIf(3 > 2, "if true, skip case")
def test_ifskip(self):
print("test if skip")
@unittest.skipUnless(3 > 2, "if true, run case")
def test_unless(self):
print("test unless test")
@unittest.expectedFailure
def test_expectedFailuer(self):
print("hahah")
if __name__ == "__main__":
unittest.main()
@unittest.skip()
装饰,跳过测试@unittest.skipIf(3 > 2, "if true, skip case")
,条件为真,跳过测试@unittest.skipUnless(3 > 2, "if true, run case")
,条件为真,执行测试同时,这些方法同样适用于测试类。
Fixture可看做夹心饼干外层的两片饼干,就是setUp/tearDown,中间的奶油就是测试用例。unittest还提供了更大范围的Fixture,如测试类和模块的Fixture
setUpModule/tearDownModule
: 整个模块的开始于结束时被执行setUpClass/tearDownClass
: 在测试类的开始与结束时被执行setUp/tearDown
: 在测试用以的开始与结束时被执行import unittest
def setUpModule():
print("test module start >>>>>>>>>>>>>>>")
def tearDownModule():
print("test module end <<<<<<<<<<<<<<<<<<<")
class MyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("test class start >>>>>>>>>>")
@classmethod
def tearDownClass(cls):
print("test class end <<<<<<<<<")
def setUp(self):
print("test case start >>>>")
def tearDown(self):
print("test case end <<<<")
def test_case1(self):
print("test case1")
def test_case2(self):
print("test case2")
if __name__ == "__main__":
unittest.main()
执行结果
Testing started at 下午 14:55 ...
D:\ENV_DIRECTOR\Envs\py3_heima\Scripts\python.exe "F:\Pycharm\PyCharm 2017.3.3\helpers\pycharm\_jb_unittest_runner.py" --target test_fixture.MyTest
Launching unittests with arguments python -m unittest test_fixture.MyTest in D:\zhenghou\python_learning\python_test
test module start >>>>>>>>>>>>>>>
test class start >>>>>>>>>>test case start >>>>
test case1
test case end <<<<
test case start >>>>
test case2
test case end <<<<
test class end <<<<<<<<<
test module end <<<<<<<<<<<<<<<<<<<
unittest单元测试框架可以将测试结果写到测试报告中,通过测试报告可以清晰地查看自动化测试用例总数、通过数、失败数以及失败原因等信息。
HTMLTestRunner.py
文件可以将HTMLTestR.py文件下载:【HTMLTestRunner】,
此前,测试用例执行是通过【TextTestRunner】类提供的run()方法完成的。这里需要把【HTMLTestRunner.py】文件中的HTMLTestRunner类替换TextTestRunner类。
建议将该py文件直接放在项目目录下。使用时直接导入
如图:
该项目下有5个test开头的测试文件,执行里面的测试用例,并生成HTML格式的测试报告。
import os
import time
import unittest
from HTMLTestRunner import HTMLTestRunner
def getAllCase():
"""获取目录下所有的test case"""
suits = unittest.defaultTestLoader.discover(start_dir=os.path.abspath(os.path.dirname(__file__)), pattern="test*.py")
return suits
def Runmain():
"""生成测试报告"""
time_str = time.strftime("%Y%m%d%H%M%S")
f = open(os.path.join(os.path.dirname(__file__), "report", time_str + "report.html"), "wb")
runner = HTMLTestRunner(stream=f, title="unittest自动化测试报告", description="基于Python-Unittest的单元测试自动化")
runner.run(getAllCase())
if __name__ == '__main__':
Runmain()
部分参数解释: