背景
搜索的索引代码已经存在数年之久了,而且使用python书写。代码上存在代码过长,逻辑过重,注释少,维护代价高等缺点。由于python代码具备简单快的特点,框架代码也是改来改去,导致了代码的维护难的问题。
单元测试的重要性
单元测试不仅可以保证部分代码的运行正确性,更是更改基础代码准确性的保证。在保证代码质量和后期重构方面可以提供强有力的保证。
单元测试的方向
在做单元测试的过程中,需要明确单元测试的方向。单元测试不可避免遇到无法覆盖所有代码怎么办,方法无返回值怎么办。遇到卡顿了该如何进行下去,代码过于庞大,写不完怎么办。尊重以下两个方向,你会找到化整为零的办法。第一,部分的测试用例也好过没有测试用例。所以,即使卡顿了,即使写不下去了,但是你已经写过的也是有价值的,不要认为没办法做到完美就是无用功。当你做完大部分的单元测试,哪些卡顿点可能也不那么复杂了。第二,不适合单元测试的要改造成适合单元测试的代码。这个很好理解,有些方法测试起来很困难,但是改造了就方便测试了。更加难测的方法也可以通过它来化解。
如何开始
操作步骤
- 在第一级目录下建立test文件夹,对需要测试的文件在test文件夹下建立同级目录
- 对测试文件命名为 test_被测试的文件名.py
- 方法类名增加test
- 每一个被测试的文件夹下增加 test_前缀
实际操作
被测试的文件
- 文件位置:/home/geeq/lxops/20200426_geeq_dslib_test/test/util/str_utils.py
- 文件名:(都是静态方法,类名命名为文件的大些,前缀加test)
- 方法名:def is_blank(s)
单元测试文件
- 文件位置:/home/geeq/lxops/20200426_geeq_dslib_test/test/util/test_str_utils.py
- 文件名:TestStrUtils(unittest.TestCase)对应测试
- 方法名:def test_is_black(self):
如何写test
基本方法:test case(测试用例0)
- 导入包unittest
- 继承unittest.TestCase
- 定义测试用例:test方法
- 使用断言方法进行判断
- 实例:静态类方法
def add(a, b):
return a+b
class TestMathFunc(unittest.TestCase):
"""Test mathfuc.py"""
def test_add(self):
"""Test method add(a, b)"""
self.assertEqual(3, mathfunc.add(1, 2))
self.assertNotEqual(3, mathfunc.add(2, 2))
if __name__ == '__main__':
unittest.main()
执行方法
- 执行整个类(会检测所有的类)
- python -m unittest test_mathfunc.TestMathFunc
- 指定测试方法
- python -m unittest test_mathfunc.TestMathFunc.test_add
- 使用注解跳过
- 在方法上使用注解
- @unittest.skip
方法初始化
-
使用背景:我们有很多的db操作,在那么多的测试方法里,如果每个都去实例化一次很复杂。有一些时间计算的操作,每一次方法前都需要初始化一下。
-
所有方法前置:
def setUp(self): print "test--------前缀方法"
-
所有方法后置:
def tearDown(self): print "test----------后置方法"
-
整个类进行前置
@classmethod def setUpClass(cls): print "test=======前缀方法"
-
整个类进行后置
@classmethod def tearDownClass(cls): print "test==========后置方法"
-
mock一个对象(模拟一个对象)
-
安装包 mock(python2需要)
def test_fuza(self): db = mock.Mock(return_value=12) self.assertEqual(12, mathfunc.fuza(10,db)
覆盖率
-
安装环境coverage
-
执行命令
coverage run --source . test_mathfunc.py
-
执行报告
coverage report -m
-
报告实例
写test流程
过滤代码
- 代码没有调用的地方
- 代码只有初始化方法
- 常量类
重构代码到可测试
- 代码直接重构后测试
- 比较重的代码工具类,转化成接口
- 函数拆分
- 可变参数拆分
- 过多参数拆分
函数中编写main方法,检查数据是否符合内心预期
- 提高unittest的测试用例的正确性
- 提高开发效率
编写单元测试原则
- 单元测试必须全部通过
- 已经写下来的单元测试不可更改
- 通用新增的方法必须要添加单元测试
- 单元测试的测试用例必须通过测试人员的确认,不完整需要补充
- 测试遇到bug,需要开发添加对应的测试用例
实战经验
普通的静态方法,带有返回值和明确入参
-
构造main方法
-
编写测试用例
-
注意用例逻辑覆盖
-
测试方法
def is_bool(s): """是否是布尔值""" if is_blank(s): return False return s in ('True', 'False', '0', '1', True, False)
-
测试用例
def test_is_bool(self): """ 有bug,self.assertTrue(StrUtils.is_bool(False))通不过。 """ self.assertFalse(StrUtils.is_bool(None)) self.assertFalse(StrUtils.is_bool('')) self.assertTrue(StrUtils.is_bool('True')) self.assertTrue(StrUtils.is_bool('False')) self.assertTrue(StrUtils.is_bool('0')) self.assertTrue(StrUtils.is_bool('1')) self.assertTrue(StrUtils.is_bool(True)) #self.assertTrue(StrUtils.is_bool(False))
间接参数的测试
-
构建间接参数对应的真实参数
-
如果是文件就构建文件
-
编写main方法测试
-
编写测试用例
-
测试方法
class ConfigConvert(object): @staticmethod def yml_convert_dict(config_path): with open(config_path) as f: return yaml.load(f) if __name__ == '__main__': cc = ConfigConvert() test_data = cc.yml_convert_dict('test.yml') print test_data
-
测试用例
class TestConfigConvert(unittest.TestCase): def test_yml_convert_dict(self): cc = config_convert.ConfigConvert() test_data = cc.yml_convert_dict('test.yml') self.assertDictEqual({'test_test': {'test_class': 'class1', 'num': 5}}, test_data) if __name__ == '__main__': unittest.main()
流程测试
-
编写main方法
-
测试调用的过程是否报错
-
没有报错即视为流程无问题
-
测试代码
def send_mail(self, data): title = data['title'] content = data['content'] system = data['system'] receivers = data.get('receivers') if not receivers: revs = system_dic[system][1] else: revs = receivers.get(1) if revs: self.mail_client.listen_mails = copy.deepcopy(revs) self.mail_client.send(subject=title, msg=content)
-
测试用例
class TestMailUtil(unittest.TestCase): def test_send(self): cfg = "mail.cfg" mail = mail_util.WYMail(cfg) mail.send('test_subject', 'test_message') if __name__ == '__main__': unittest.main()
没有返回参数的测试
-
阅读逻辑处理流程
-
构建中间状态存储对象
-
检查方法变化过程中对象变化
def timing_load_dict(reload_dict, interval, **kwargs): """ 定时reload dict reload_dict: 调用reload dict的具体方法 interval: reload的时间间隔,单位为s,默认为一天 """ while True: time.sleep(interval) if kwargs: reload_dict(**kwargs) else: reload_dict()
-
测试用例
-
构建中间对象代码
class Test: def __init__(self, num): self.num = num self.num_dict = {} self.num_dict[self.num] = self.num def test_method(self, number=5): self.num +=1 self.num_dict[self.num] = self.num time.sleep(number) if self.num == 2: raise Exception("stop")
-
实际测试用例代码
def test_timing_load_dict(self): test_ins = Test(0) try: asynchronous_util.timing_load_dict(test_ins.test_method, interval=1, number=3) except: print test_ins.num_dict self.assertDictEqual({0:0,1:1,2:2}, test_ins.num_dict)
循环方法的测试
- 确认是否有中间状态
- 编写main方法,打印循环内容
- 制造断开条件
- 编写测试用例
def timing_load_dict(reload_dict, interval, **kwargs): """ 定时reload dict reload_dict: 调用reload dict的具体方法 interval: reload的时间间隔,单位为s,默认为一天 """ while True: time.sleep(interval) if kwargs: reload_dict(**kwargs) else: reload_dict()
- 构建中间对象代码(这里的exception就是断开条件)
class Test: def __init__(self, num): self.num = num self.num_dict = {} self.num_dict[self.num] = self.num def test_method(self, number=5): self.num +=1 self.num_dict[self.num] = self.num time.sleep(number) if self.num == 2: raise Exception("stop")
- 实际测试用例代码
def test_timing_load_dict(self): test_ins = Test(0) try: asynchronous_util.timing_load_dict(test_ins.test_method, interval=1, number=3) except: print test_ins.num_dict self.assertDictEqual({0:0,1:1,2:2}, test_ins.num_dict)
异步的测试
-
阅读测试代码
-
构建异步条件
-
构建中间存储状态,存储异步状态
-
对比中间状态的成功与否
def parallel_load_dict(reload_dict, interval=24*60*60, **kwargs): """ 并行调用定时调用reload dict的程序 """ def callback(future): ex = future.exception() if ex is not None: print ex return_future = EXECUTOR.submit(timing_load_dict, reload_dict, interval, **kwargs) return_future.add_done_callback(callback)
-
构建中间对象代码
class Test: def __init__(self, num): self.num = num self.num_dict = {} self.num_dict[self.num] = self.num def test_method(self, number=5): self.num +=1 self.num_dict[self.num] = self.num time.sleep(number) if self.num == 2: raise Exception("stop")
-
测试用例
def test_parallel_load_dict(self): test_ins = Test(0) num = 6 self.assertEqual(test_ins.num, 0) asynchronous_util.parallel_load_dict(test_ins.test_method, interval=num, number=num) time.sleep(1) self.assertEqual(test_ins.num, 0) time.sleep(num) self.assertEqual(test_ins.num, 1) time.sleep(num*2) self.assertEqual(test_ins.num, 2)