单元测试概述
- 什么是单元测试(白盒测试)?
- 单元测试是开发者编写的一小段代码,用于检验被测代的一个很小的、很明确的功能是否正确.。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。
- 单元测试什么时候开始?
- 越早介入越好
- 单元测试由谁负责?
- 由开发负责
单元测试需要注意
- 单元测试的时候一个大前提就是需要清楚的知道,自己要测试的程序块所预期的输入和输出,然后根据这个预期和程序逻辑来书写case。这里的预期结果一定要针对需求/设计的逻辑去写,而不是针对程序的实现去写,否则单测就失去了意义,照着错误的实现设计出的case也很可能是错的。
单元测试框架列举及国内大厂现状
- Unittest
- Python内置的标准类库。它的API跟Java的JUnit、.net的NUnit,C++的CppUnit很相似
- Pytest
- 丰富、灵活的测试框架,语法简单,可以结合allure生成一个炫酷的测试报告,现在比较主流
- Nose
- Nose是对unittest的扩展,使得python的测试更加简单
- Mock
- unittest.mock是用来测试python的库。这个是一个标准库(出现在python3.3版本以后)
单元测试覆盖率
- 代码覆盖率也被用于自动化测试和手工测试来度量测试是否全面的指标之一,应用覆盖率的思想增强测试用例的设计
- 单元测试覆盖类型:
- 语句覆盖
- 条件覆盖
- 判断覆盖
- 路径覆盖
- 被测试代码片段
def demo_method(a,b,x):
if (a>1 and b == 0):
x = x/a
if (a == 2 or x>1):
x = x+1
return x
语句覆盖
-
语句覆盖定义
- 通过设计一定量的测试用例,保证被测试的方法每一行代码都会被执行一遍
- 运行测试用例的时候被击中的代码行即称为被覆盖的语句
-
测试用例
- 仅需要一条case,即可实现行覆盖
a =3, b=0, x=4
-
漏洞
- and -> or
行覆盖是一个最基础的覆盖方式,但是也是最薄弱的,如果完全依赖行覆盖,会出现很严重的问题
判断覆盖
- 判断覆盖定义
- 运行测试用例的过程中被击中的判定语句
-
测试用例
- 漏洞
- 大部分的判定语句是由多个逻辑条件组合而成,若仅仅判断其整个最终结果,而忽略每个条件的取值情况,必然会遗漏部分测试路径
- a==2 or x>1 -> a==2 or x<1
条件覆盖
- 条件覆盖定义
- 条件覆盖和判定覆盖类似,不过判定覆盖关注整个判定语句,而条件覆盖则关注某个判断条件
-
测试用例:if(a>1 and b ==0)
- 缺陷
- 测试用例指数级增加(2**conditions)
路径覆盖
- 路径覆盖定义
- 覆盖所有可能执行的路径
-
测试用例
- 应用这些方法设计测试用例
unittest框架介绍
- 官网: https://docs.python.org/3/library/unittest.html#unittest.TextTestRunner
- python自带的单元测试框架,常用在单元测试
- 在自动化测试中提供用例组织与执行
- 提供丰富的断言方法-验证函数等功能
- 加上HTMLTestRunner可以生成html的报告
unittest编写与规范
- Unittest提供了了test cases、test suites、test fixtures、test runner相关的组件
- 编写规范
- 测试模块首先import unittest
- 测试类必须继承unittest.TestCase
- 测试方法必须以"test_"开头
- 类名使用“驼峰”命名法
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
测试框架结构
- 总结:
- setUp用来为测试准备环境, tearDown用来清理环境
- 如果想要在所有case执行之前准备一次环境,并在所有case执行结束之后再清理环境,我们可以用setUpClass() 与 tearDownClass();比如:数据库连接及销毁
- 如果想有些方法不在本次执行使用
@unittest.skip
- 测试方法的命名:以test开头
- 各种执行-单一用例,全部
class TestClass(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print("这是测试整个类前要执行的方法")
def setUp(self) -> None:
print("这是每一个测试方法前面运行的方法")
def tearDown(self) -> None:
print("这是每一个测试方法后面运行的方法")
@unittest.skip("这次不想执行这个测试")
def test_second(self):
print("这是测试方法2")
@classmethod
def tearDownClass(cls) -> None:
print("这是测试整个类后要执行的方法")
if __name__ == '__main__':
unittest.main()
Tips:
-> None
表示默认返回值为None
。setUp 和 tearDown 是在每条测试用例的前后调用,setUpClass
和tearDownClass
是在整个测试类的前后调用,注意setUpClass
和tearDownClass
要加上装饰器@classmethod
。unittest.main()
可以调用当前模块所有的测试类和测试用例。
setUp 与 tearDown
- 基于测试方法级别的setUp,tearDown
- 执行每个测试方法的时候都会执行一次setUp和tearDown
- 基于类级别的setUpClass,tearDownClass
- 执行这个类里面的所有测试方法只有一次执行setup, tearDown。
unittest断言
- 用unittest组件测试用例的时候,断言的方法还是很多的,下面介绍几种常用的断言方法: assertEqual、assertln、assertTrue
- 基本断言方法
-
基本的断言方法提供了测试结果是True还是False。所有的断言方法都有一个msg参数,如果指定msg参数的值,则将该信息作为失败的错误信息返回。
-
Tips:Pycharm中运行测试用例后程序会自动记住上一次的执行代码,这时候需要去右上角点击项目将运行历史清空。
unittest执行测试用例
-
编写unittest测试用例的原则
- unittest会自动识别以test开头的函数是测试代码,test一定要是小写
-
执行测试用例的顺序
- 测试用例执行顺序是以test后面的字母顺序执行的。例如test_a,test_b,test_c
多个测试用例的集合就是测试套件,通过测试套件来管理多个测试用例
-
执行方法一:
- unittest.main()
-
执行方法二:加入容器中执行
- suite=unittest.TestSuite()
- suite.addTest(TestMethod("test_01"))
- suite.addTest(TestMethod("test_02"'))
- unittest.TextTestRunner().run(suite)
-
执行方法三:此用法可以同时测试多个类
- suite1 = unittest.TestLoader().loadTestsFromTestCase(TestCase1)
- suite2 = unittest.TestLoader().loadTestsFromTestCase(TestCase2)
- suite = unittest.TestSuite([suite1, suite2])
- unittest.TextTestRunner(verbosity=2).run(suite)
执行方法四:匹配某个目录下所有以test开头的py文件,执行这些文件下的所有测试用例
test_dir = "./ test_case"
-
discover = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py")
- discover可以一次调用多个脚本
- test_dir 被测试脚本的路径
- pattern脚本名称匹配规则
import unittest
class Search():
def search(self):
print("search")
class TestSearch(unittest.TestCase):
def setUp(self) -> None:
print("setUp_方法级别")
self.search = Search()
def tearDown(self) -> None:
print("tearDown_方法级别")
self.search = Search()
@classmethod
def setUpClass(cls) -> None:
print("setUp_类级别")
cls.search = Search()
@classmethod
def tearDownClass(cls) -> None:
print("tearDown_类级别")
cls.search = Search()
def test_search1(self):
print("search1")
assert True == self.search.search()
def test_equal(self):
print("断言相等")
self.assertEqual(1,2,"判断1==2")
def test_noteqal(self):
print("断言不相等")
self.assertNotEqual(1,2,"判断1!=2")
class TestSearch1(unittest.TestCase):
def setUp(self) -> None:
print("setUp_方法级别")
self.search = Search()
def tearDown(self) -> None:
print("tearDown_方法级别")
self.search = Search()
@classmethod
def setUpClass(cls) -> None:
print("setUp_类级别")
cls.search = Search()
@classmethod
def tearDownClass(cls) -> None:
print("tearDown_类级别")
cls.search = Search()
def test_search1(self):
print("search1")
assert True == self.search.search()
def test_equal(self):
print("断言相等")
self.assertEqual(1,2,"判断1==2")
def test_noteqal(self):
print("断言不相等")
self.assertNotEqual(1,2,"判断1!=2")
if __name__ == '__main__':
# 方法一:运行所用测试用例
# unittest.main()
# 方法二:测试套件
# 创建一个测试套件,testsuite
suite = unittest.TestSuite()
# 只运行TestSearch1下的test_search1测试用例
suite.addTest(TestSearch1("test_search1"))
# 执行用例
unittest.TextTestRunner().run(suite)
# 方法三:执行某个测试类
suite1 = unittest.TestLoader().loadTestsFromTestCase(TestSearch1)
suite2 = unittest.TestLoader().loadTestsFromTestCase(TestSearch)
suite = unittest.TestSuite([suite1, suite2])
unittest.TextTestRunner(verbosity=2).run(suite)
Tips:Pycharm运行时IDE不太准确,可以在Pycharm中切换到命令行里面运行,如:
python test_search.py
D:\0_NSFocus_My_Files\5_Python_Test\Unittest_Demo>python test_unittest.py
setUp_类级别
setUp_方法级别
search1
search
tearDown_方法级别
FtearDown_类级别
======================================================================
FAIL: test_search1 (__main__.TestSearch1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_unittest.py", line 63, in test_search1
assert True == self.search.search()
AssertionError
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (failures=1)
-
执行方法四:
目录结构:
import unittest
if __name__ == '__main__':
test_dir = './testcases'
discover = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py")
unittest.TextTestRunner(verbosity=2).run(discover)
-
测试用例执行过程
unittest实战
- Unittest 安装
- unittest 使用示例
- unittest 结合 htmltestrunner 生成带日志的测试报告
- http://tungwaiyip.info/software/HTMLTestRunner.html (python2版本)
- https://github.com/huilansame/HTMLTestRunner_PY3 (python3版本)
import unittest
from Unittest_Demo.HTMLTestRunner import HTMLTestRunner
if __name__ == '__main__':
report_titile = '用例执行报告 我的标题'
desc = '用于展示修改样式后的HTMLTestRunner'
report_file = './result.html'
test_dir = './testcases'
discover = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py")
unittest.TextTestRunner(verbosity=2).run(discover)
with open(report_file,'wb') as report:
runner = HTMLTestRunner(stream=report, title=report_titile,description=desc)
runner.run(discover)
下一节:pytest 测试框架,包括测试用例管理框架、fixture机制、断言机制。