Python 单元测试详解

本文详细介绍了Python单元测试框架unittest、nose和pytest的使用,包括基本概念、编写规则、参数化、断言和Mock工具。此外,还讨论了单元测试覆盖率报告的生成,并通过具体代码示例展示了如何进行单元测试。最后,文章提供了一个情景示例,涵盖了从服务层到数据库模拟和Mock的测试实践。
摘要由CSDN通过智能技术生成

作者:yukkizhang,腾讯 CSIG 测试工程师

本文直接从常用的 Python 单元测试框架出发,分别对几种框架进行了简单的介绍和小结,然后介绍了 Mock 的框架,以及测试报告生成方式,并以具体代码示例进行说明,最后列举了一些常见问题。

一、常用 Python 单测框架

若你不想安装或不允许第三方库,那么 unittest 是最好也是唯一的选择。反之,pytest 无疑是最佳选择,众多 Python 开源项目(如大名鼎鼎的 requests)都是使用 pytest 作为单元测试框架。甚至,连 nose2 在官方文档上都建议大家使用 pytest。我们知道,nose 已经进入了维护模式,取代者是 nose2。相比 nose2,pytest 的生态无疑更具优势,社区的活跃度也更高。

总体来说,unittest 用例格式复杂,兼容性无,插件少,二次开发方便。pytest 更加方便快捷,用例格式简单,可以执行 unittest 风格的测试用例,较好的兼容性,插件丰富。

二、unittest

1. 基本概念

unittest 中最核心的四个概念是:**test fixture、test case、test suite、test runner **。

  • test fixture:表示执行一个或多个测试所需的准备,以及任何关联的清理操作。例如这可能涉及创建临时或代理数据库、目录或启动服务器进程。

  • test case:测试用例是最小的测试单元。它检查特定的输入集的响应。单元测试提供了一个基类测试用例,可用于创建新的测试用例。

  • test suite:测试套件是测试用例、测试套件或两者的集合,用于归档需要一起执行的测试。

  • test runner:是一个用于执行和输出结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。

2. 编写规则
  • 编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

  • test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

  • 对每一类测试都需要编写一个test_xxx()方法。

3. 简单示例
3.1 目录结构
$ tree .
.
├── README.md
├── requirements.txt
└── src
    ├── demo
    │   └── calculator.py
    └── tests
        └── demo
            ├── __init__.py
            ├── test_calculator_unittest.py
            └── test_calculator_unittest_with_fixture.py
3.2 计算器实现代码
class Calculator:

    def add(self, a, b):
        return a + b

    def sub(self, a, b):
        return a - b

    def mul(self, a, b):
        return a * b

    def div(self, a, b):
        return a / b

3.3 计算器测试代码
import unittest

from src.demo.calculator import Calculator


class TestCalculator(unittest.TestCase):
    def test_add(self):
        c = Calculator()
        result = c.add(3, 5)
        self.assertEqual(result, 8)

    def test_sub(self):
        c = Calculator()
        result = c.sub(10, 5)
        self.assertEqual(result, 5)

    def test_mul(self):
        c = Calculator()
        result = c.mul(5, 7)
        self.assertEqual(result, 35)

    def test_div(self):
        c = Calculator()
        result = c.div(10, 5)
        self.assertEqual(result, 2)


if __name__ == '__main__':
    unittest.main()
3.4 执行结果
Ran 4 tests in 0.002s

OK
4. 用例前置和后置

基于 unittest 的四个概念的理解,上述简单用例,可以修改为:

import unittest

from src.demo.calculator import Calculator


class TestCalculatorWithFixture(unittest.TestCase):
    # 测试用例前置动作
    def setUp(self):
        print("test start")

    # 测试用例后置动作
    def tearDown(self):
        print("test end")

    def test_add(self):
        c = Calculator()
        result = c.add(3, 5)
        self.assertEqual(result, 8)

    def test_sub(self):
        c = Calculator()
        result = c.sub(10, 5)
        self.assertEqual(result, 5)

    def test_mul(self):
        c = Calculator()
        result = c.mul(5, 7)
        self.assertEqual(result, 35)

    def test_div(self):
        c = Calculator()
        result = c.div(10, 5)
        self.assertEqual(result, 2)


if __name__ == '__main__':
    # 创建测试套件
    suit = unittest.TestSuite()
    suit.addTest(TestCalculatorWithFixture("test_add"))
    suit.addTest(TestCalculatorWithFixture("test_sub"))
    suit.addTest(TestCalculatorWithFixture("test_mul"))
    suit.addTest(TestCalculatorWithFixture("test_div"))
    # 创建测试运行器
    runner = unittest.TestRunner()
    runner.run(suit)
5. 参数化

标准库的 unittest 自身不支持参数化测试,可以通过第三方库来支持:parameterized 和 ddt。

其中 parameterized 只需要一个装饰器@parameterized.expand,ddt 需要三个装饰器@ddt、@data、@unpack,它们生成的 test 分别有一个名字,ddt 会携带具体的参数信息。

5.1 parameterized
import unittest

from parameterized import parameterized, param

from src.demo.calculator import Calculator


class TestCalculator(unittest.TestCase):
    @parameterized.expand([
        param(3, 5, 8),
        param(1, 2, 3),
        param(2, 2, 4)
    ])
    def test_add(self, num1, num2, total):
        c = Calculator()
        result = c.add(num1, num2)
        self.assertEqual(result, total)


if __name__ == '__main__':
    unittest.main()

执行结果:

test_add_0 (__main__.TestCalculator) ... ok
test_add_1 (__main__.TestCalculator) ... ok
test_add_2 (__main__.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
5.2 ddt
import unittest

from ddt import data, unpack, ddt

from src.demo.calculator import Calculator

@ddt
class TestCalculator(unittest.TestCase):
    @data((3, 5, 8),(1, 2, 3),(2, 2, 4))
    @unpack
    def test_add(self, num1, num2, total):
        c = Calculator()
        result = c.add(num1, num2)
        self.assertEqual(result, total)


if __name__ == '__main__':
    unittest.main()

执行结果:

test_add_1__3__5__8_ (__main__.TestCalculator) ... ok
test_add_2__1__2__3_ (__main__.TestCalculator) ... ok
test_add_3__2__2__4_ (__main__.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
6. 断言

unittest 提供了丰富的断言,常用的包括:

assertEqual、assertNotEqual、assertTrue、assertFalse、assertIn、assertNotIn 等。

具体可以直接看源码提供的方法:

enter image description here

三、nose

nose 已经进入维护模式,从github nose上可以看到,nose 最近的一次代码提交还是在 2016 年 5 月 4 日。

继承 nose 的是 nose2,但要注意的是,nose2 并不支持 nose 的全部功能,它们的区别可以看这里。nose2 的主要目的是扩展 Python 的标准单元测试库 unittest,因此它的定位是“带插件的 unittest”。nose2 提供的插件,例如测试用例加载器,覆盖度报告生成器,并行测试等内置插件和第三方插件,让单元测试变得更加完善。

nose2 的社区没有 pytest 的活跃,要使用高级框架,推荐使用 pytest,因此下文不做过多详述。

1. 编写规则
  • nose2 的测试用例并不限制于类,也可以直接使用函数。

  • 任何函数和类,只要名称匹配一定的条件(例如,以 test 开头或以 test 结尾等),都会被自动识别为测试用例;

  • 为了兼容 unittest, 所有的基于 unitest 编写的测试用例,也会被 nose 自动识别为。

2. 简单示例
2.1 计算器代码

参考 unittest 的计算器代码部分。

2.2 计算器测试代码
import nose2

from src.demo.calculator import Calculator


def test_add():
    c = Calculator()
    result = c.add(3, 5)
    assert result == 8


def test_sub():
    c = Calculator()
    result = c.sub(10, 5)
    assert result == 5


def test_mul():
    c = Calculator()
    result = c.mul(5, 7)
    assert result == 35


def test_div():
    c = Calculator()
    result = c.div(10, 5)
    assert result == 2


if __name__ == '__main__':
    nose2.main()

2.3 执行结果
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK
3. 参数化
import nose2
from nose2.tools import params

from src.demo.calculator import Calculator

test_data = [
    {"nums": (3, 5), "total": 8},
    {"nums": (1, 2), "total": 3},
    {"nums": (2, 2), "total": 4}
]


@params(*test_data)
def test_add(data):
    c = Calculator()
    result = c.add(*data['nums'])
    assert result == data['total']


if __name__ == '__main__':
    nose2.main()

四、pytest

1. 编写规则
  • 测试文件以 test_开头(以 test 结尾也可以)

  • 测试类以 Test 开头,并且不能带有 init 方法

  • 测试函数以 test_开头

  • 断言使用基本的 assert 即可

可以通过下面的命令,查看 Pytest 收集到哪些测试用例:

$ py.test --collect-only
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值