单元测试指导实践

内容纲要

背景

搜索的索引代码已经存在数年之久了,而且使用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流程

过滤代码

  1. 代码没有调用的地方
  2. 代码只有初始化方法
  3. 常量类

重构代码到可测试

  • 代码直接重构后测试
  • 比较重的代码工具类,转化成接口
  • 函数拆分
    • 可变参数拆分
    • 过多参数拆分

函数中编写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)

发表评论

电子邮件地址不会被公开。 必填项已用*标注