Python 单元测试

软件测试即是一种实际输出与预期输出间的审核或者比较过程

什么是单元测试

  1. 什么是单元

    代码逻辑中的最小可复用部分:类、函数等

  2. 单元测试的目标

    详细验证所有(一般以代码覆盖率为指标)实现代码的执行逻辑,尤其是各种边界条件

为什么要写单元测试

程序单元作为应用的最小可测试部件。测试能够保证程序的强壮性,也能促使重新思考代码的设计,避免写出耦合严重的代码。

简单来说,就是能够确保代码质量,能够帮助我们较早地发现 Bug;能够改善代码设计,难以测试的代码一般是设计不够简洁的代码;保证重构不会引入新问题,每次修改之前代码的时候,只需重新跑测试代码就能避免新改动的代码对现有代码产生影响。

何时进行单元测试

  • 扩展实现代码时(实现新功能时)
  • 修改现有代码时(代码重构)
  • 任何代码提交时(CI)

如何保证单元测试的隔离性

  1. 单元测试的隔离性问题

    一般我们写的代码,常基于别人的 API 代码构建,同时,我们的代码也常作为别人的 API 代码被调用。在进行单元测试时,实际外部依赖可能包含复杂计算或者存在网络请求,成本较高,不适合重复执行。例如,我们代码依赖的 API 并没有被实现和被提供,比如我们的代码作为C/S模式中的客户端依赖服务端的某个 HTTP 服务;或者,我们的代码运行时可能会对周围代码产/生负面影响,比如发送一个修改数据库数据、发送电子邮件,显然不适合每次执行都真实地进行这些数据修改或者网络请求。

  2. 如何保证单元测试的隔离性

  • 使用 Mock/Stub/Fake 替换外部依赖
  • 指定替换逻辑的行为
  • 验证带测试逻辑的行为

Python 单元测试相关包

  • unittest: Python 内置库,优点是简洁易用,缺点是使用时比较繁琐
  • pytest:上手容易,使用起来简洁高效
  • mock: 替换掉网络调用或者 rpc 请求等

例子

下面是一个关于几何圆的代码单元测试例子,以说明单元测试在修改现有代码时的作用。

  1. 圆的实现代码 (circle.py)

    # -*- coding: utf-8 -*-
    # @File    : circle.py
    from math import pi
    # pi = 3.14 # 后面会修改代码所依赖的 PI 的值,并进行单元测试
    
    class Circle(object):
        """docstring for Circle"""
    
        def __init__(self, radius):
            super(Circle, self).__init__()
            self.radius = radius
    
        @property
        def diameter(self):
            """
            圆的直径
            :return:  直径
            """
            return 2 * self.radius
    
        @diameter.setter
        def diameter(self, value):
            """
            设置直径
            :param value: 直径的值
            :return: None
            """
            self.radius = value / 2.0
    
        @diameter.deleter
        def diameter(self):
            raise AttributeError("Can't delete attribute")
    
        @property
        def perimeter(self):
            """
            圆的周长
            :return: 周长
            """
            return 2 * pi * self.radius
    
        @perimeter.setter
        def perimeter(self, value):
            self.radius = value / pi / 2.0
    
        @perimeter.deleter
        def perimeter(self):
            raise AttributeError("Can't delete attribute")
    
        @property
        def area(self):
            """
            圆的面积
            :return: 面积
            """
            return pi * (self.radius ** 2)
    
        @area.setter
        def area(self, value):
            self.radius = (value / pi) ** (1 / 2)
    
        @area.deleter
        def area(self):
            raise AttributeError("Can't delete attribute")
    
    
    def print_circle_info(c1):
        print("radius: {}".format(c1.radius))
        print("diameter: {}".format(c1.diameter))
        print("perimeter: {}".format(c1.perimeter))
        print("area: {}".format(c1.area))
    
    
    if __name__ == '__main__':
        c1 = Circle(1)
        print_circle_info(c1)
    
        print("\n================")
        print("set diameter to 4")
        print("================")
        c1.diameter = 4
        print_circle_info(c1)
    
        print("\n================")
        print("set perimeter to 6.28")
        print("================")
        c1.perimeter = 6.28
        print_circle_info(c1)
    
  2. 单元测试代码 ( test_circle_pytest.py )

    # -*- coding: utf-8 -*-
    # @File    : test_circle_pytest.py
    import pytest
    
    from circle import Circle
    
    
    class TestCirclePytest:
        def test_init(self):
            c0 = Circle(1)
            assert c0.radius == 1
    
        def test_diameter(self):
            c0 = Circle(2)
            assert c0.diameter == 4
    
            c0.diameter = 8
            assert c0.diameter == 8
    
            with pytest.raises(AttributeError):
                del c0.diameter
    
        def test_perimeter(self):
            c0 = Circle(1)
            assert c0.perimeter - 6.28318 == pytest.approx(0, abs=1E-5)
    
            c0.perimeter = 6.28
            assert c0.perimeter == 6.28
            assert abs(c0.radius - 1.0) == pytest.approx(0, abs=0.01)
    
            with pytest.raises(AttributeError):
                del c0.perimeter
    
        def test_area(self):
            c0 = Circle(1)
            assert c0.area - 3.14159 == pytest.approx(0, abs=1E-5)
    
            c0.area = 3.14
            assert c0.area == 3.14
    
            with pytest.raises(AttributeError):
                del c0.area
    
  3. 代码实现后的单元测试结果

    unittest

    代码实现后,运行单元测试代码,显然,结果显示代码实现基本没问题

  4. 修改代码中的某些部分

    实际写代码的时候,总会因为某些需求,会需要修改现已实现的代码。为了模拟,这种需求,这里我们修改代码所依赖的 PI 值。修改 circle.py 中的 PI 值为 3.14 :

    # -*- coding: utf-8 -*-
    # @File    : circle.py
    # from math import pi  # 去掉之前的 PI 值的定义
    pi = 3.14
    
    其他代码省略 ...
    
  5. 修改代码后重新运行单元测试代码

unittest2
从运行结果可以看到,修改 pi 的值后,单元测试中部分测试样例没有通过,分析可以发现,主要是因为 pi 值的精度达不到我们测试代码的要求。将 PI 值修改为 3.141592526 显然就可以,甚至可以一点点增加 PI 的精度,直到通过单元测试。

参考链接

  1. Python 单元测试 —— 知乎

  2. 单元测试 —— 廖雪峰的个人网站

    • 单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
    • 单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
    • 单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
    • 单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。
  3. python单元测试框架pytest简介 —— CSDN 博客

  4. Python测试框架 - Pytest

  5. 用 pytest 测试 python 代码

  6. Mock 在 Python 单元测试中的使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值