自动化测试之单元测试
单元测试:
通常而言,一个单元测试用例是用于判断某个特定条件或场景下某个特定函数的行为
直观描述:针对一个函数,构造不同输入,验证函数的输出是否符合预期
但需要注意:输入并非都是显性输入,存在隐性输入(读取到的文件或数据库数据)
单元测试的意义:
从质量角度:
- 测试针对性强,bug更容易暴露
- 场景构造简单,核心功能验证更充分
- 保证代码结构良好,具有较高的可测性和可维护性
从效率角度:
- 测试场景构建快捷,减少调试时间
- 只针对修改的代码展开测试,减少测试时间
- 更多bug在项目早期被发现,缩短开发周期
Unitest基本概念
- test 测试用例? :针对一个特定场景,特定目的的具体测试用例
- testcase? 测试类? :可以包含同一个测试对象的多个测试用例(比如一个函数的不同输入)
- testsuite? 测试集? :可以包含多个测试类的多个测试用例
- Assertion? 断言? ?:必须使用断言判断测试结果
- testfixture:为测试做统一的初始化和清除工作
- setUp? ?在测试类的每个测试用例执行前执行
- tearDown? 在测试类的每个测试用例执行后执行
- setUpClass? 在测试类的第一个测试用例执行前执行
- tearDownClass 在测试类的最后一个测试用例执行后执行
执行顺序示意如下:(注意两两不需要配对出现)
setUpClass | ????????setUp | ????????????????测试用例1 | ????????tearDown | ????????setUp | ????????????????测试用例2 | ????????tearDown | ????????…… | tearDownClass |
示例1:?
#! person.py
class Person(object):
def __init__(self,name):
self.name=name
def get_name(self):
return self.name
#!persontest.py
import unittest
import person
class PersonTestCase(unittest.TestCase):
def setUp(self):
#初始化
self.p1=person.Person('zhang')
self.p2=person.Person('li')
def test_get_name(self)#必须test开头才会被认为是测试用例
self.assertEqual(self.p1.get_name(),'zhang') #断言判断是否一致
self.assertEqual(self.p1.get_name(),'li')
示例2:
#! company.py
class Company(object):
def __init__(self,name,boss):
self.name=name
self.boss=boss
self.staffs=set()
def who is boss(self):
return boss.get_name()
def hire(self,person)#雇佣新员工
'''
……
'''
def fire(self,person)#解雇员工
'''
……
'''
#! companytest.py
import company
import person
import unittest
class CompanyTestCase(unittest.TestCase):
@classmethod #必须使用类class装饰器才能使用 setUpClass
def setUpClass(self):
self.boss=person.Person('wang')
self.acg=company.Company('ACG',self.boss)
def tearDown(self):
self.acg.staffs.clear()
def test_who_is_boss(self):#测试用例 老板姓名是否一致
self.assertEqual(self.acg.who_is_boss(),'wang')
def test_number_of_staffs_increase_when_hire(self):#测试用例 每雇佣一个人员工数是否加一
self.acg.hire(person.Person('li'))
self.assertEqual(len(self.acg.staffs),1)
self.acg.hire(person.Person('zhang'))
self.assertEqual(len(self.acg.staffs),2)
def test_number_of_staffs_notchange_when_hire(self):#测试用例 雇佣相同一个人员工数是否增加
self.acg.hire(person.Person('li'))
self.assertEqual(len(self.acg.staffs),1)
self.acg.hire(person.Person('li'))
self.assertEqual(len(self.acg.staffs),1)
运行Unittest
if __name__=='__main__':
unittest.main()
执行输出
·(点) | 测试正确(测试通过) | F | 测试失败 | E | 测试异常(给出断言错误) |
运行测试集
testsuite? 测试集? :可以包含多个测试类的多个测试用例
def MyTestSuite():
suite=unittest.TestSuite()
#只针对部分测试类的其中一个测试用例进行测试
suite.addTest(PersonTestCase('test_get_name'))
suite.addTest(CompanyTestCase('test_who_is_boss'))
return suite
if __name__=='__main__':
unittest.main(defaultTest='MyTestSuite')
单元测试规范
- 必须使用断言判断结果 unittest.TestCase.assert
assertEqual? /? assertNotEqual | 判断相等或不等 | assertTrue? / assertFalse | 判断boolean(True? False) | assertRaises | 判断抛出异常是否符合预期 |
- 测试用例需要有自表述能力 ,达到见名知意
- 测试用例之间相互独立,不应相互依赖,相互调用
- 一个测试用例只测一个函数?
- 代码需要保持一致性(非必要不引入 time.time()和random.random())
统计单元测试覆盖率
coverge工具
替换python test.py arg1 arg2 为 coverge run --branch test.py arg1 arg2
coverge report -m --omit=file1,file2
coverge report -m 获取当前目录下.coverge文件
Stmts | 总行数 | Miss | 未覆盖行数 | Branch | 总分支行数 | BrPart | 未覆盖分支数 | Cover | 覆盖率 | Missing | 未覆盖具体信息(未覆盖行号信息) |
进一步信息可视化(直观便捷的HTML报告)
coverge html --omit=file1,file2
Mock工具
为什么要使用Mock?
- 要测模块A,但它要调用的模块B还未开发完成
- 代码中有结果不可预知的代码(随机数 当前时间)
- 其他模块的质量不可靠
Mock目的:
常见场景:
关键字:return_value
支持python常见数据类型
#! mockdemo01.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
boss.get_name=mock.Mock(return_value='li')
print shoeshop.who_is_boss()
boss.get_name=mock.Mock(return_value=12345)
print shoeshop.who_is_boss()
boss.get_name=mock.Mock(return_value=[1,2,'4',5])
print shoeshop.who_is_boss()
- 场景2:根据调用次数返回想要的结果? 每次调用测试返回结果都可以不一致
关键字:side_effect
超出调用次数抛StopIteration异常
#! mockdemo02.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
boss.get_name=mock.Mock(side_effect=['li',12345])
print shoeshop.who_is_boss() #li
print shoeshop.who_is_boss() #12345
print shoeshop.who_is_boss() #StopIteration异常 超过设定调用次数
关键字:side_effect
替换函数逻辑
#! mockdemo03.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
def new_logic(arg):
pairs={'a':123,
True:'abc'
}
return pairs[arg]
boss.get_name=mock.Mock(side_effect=new_logic)
print boss.get_name(True) #abc
print boss.get_name('a') #123
关键字:side_effect
#! mockdemo04.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
boss.get_name=mock.Mock(side_effect=KeyError('keyerror'))
print shoeshop.who_is_boss() #抛出异常 keyerror
关键字:with? patch.object
#! mockdemo05.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
print shoeshop.who_is_boss() #zhang
with mock.patch.object(person.Person,'get_name',return_value='li'):
print shoeshop.who_is_boss() #li
print shoeshop.who_is_boss() #zhang
called | 函数是否被调用(Boolean) | call_count | 函数被调用次数 | call_args | 函数被调用最后一次参数 | call_args_list | 函数被调用参数列表 |
#! mockdemo06.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
boss.get_name=mock.Mock(side_effect=boss.get_name)
print boss.get_name.called #False
print boss.get_name.call_count #0
print boss.get_name.call_args #None
print boss.get_name.call_args_list #[]
print shoeshop.who_is_boss()
print shoeshop.who_is_boss()
print shoeshop.who_is_boss()
print boss.get_name.called #True
print boss.get_name.call_count #3
print boss.get_name.call_args #call()
print boss.get_name.call_args_list #[call(),call(),call()]
- 场景7 在返回值改变的同时,确保API不会因为mock改变
关键字:create_autospec
#! mockdemo07.py
import company
import person
import mock
boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)
shoeshop.who_is_boss=mock.create_autospec(shoeshop.who_is_boss,return_value='li')
print shoeshop.who_is_boss() #li
print shoeshop.who_is_boss('abc') #抛出异常
适用于依赖模块只定义了接口,但尚未开发场景
#! mockdemo08.py
import company
import mock
#company模块中调用的person模块尚未开发 需要构造
boss=mock.Mock()
boss.get_name=mock.Mock(return_value='zhang')
shoeshop=company.Company('shoe shop',boss)
print shoeshop.who_is_boss() #zhang
函数调用链:os.popen(cmd).read().split()
Mock方法:
os.popen=mock.Mock()
os.popen.return_value.read.return value.split.return_value=xx
#! mockdemo09.py
import os
import mock
print os.popen('hostname').read().split() #['host']
os.popen=mock.Mock()
os.popen.return_value.read.return value.split.return_value='mock'
print os.popen('hostname').read().split() #mock
|