Skip to main content

Python 代码测试

Python 单元测试

单元测试是软件开发中确保代码质量的重要环节。本章节详细介绍Python单元测试的各个方面,包括unittest和pytest框架、测试编写技巧、模拟(mocking)以及测试覆盖率分析。

1. 单元测试基础

1.1 什么是单元测试?

单元测试是指对软件中最小可测试单元(如函数或方法)进行验证,以确保其按预期工作。单元测试通常是自动化的,运行快速且独立于其他代码。

1.2 为什么需要单元测试?

  • 确保代码正确性:验证代码是否按预期运行。
  • 便于重构:在修改代码时,快速检查是否引入新错误。
  • 提高代码质量:通过测试驱动开发(TDD),编写更可靠的代码。
  • 文档化:测试用例可以作为代码功能的示例。

2. 使用unittest框架

Python内置的unittest模块是学习单元测试的起点,适合理解测试的基本结构。

2.1 编写第一个单元测试

假设我们有一个简单的函数,用于计算两个数的和。我们将为其编写测试。

# calculator.py
def add(a, b):
    return a + b

以下是对应的单元测试代码:

# test_calculator_unittest.py
import unittest
from calculator import add

class TestCalculator(unittest.TestCase):
    def test_add_positive_numbers(self):
        result = add(2, 3)
        self.assertEqual(result, 5, "2 + 3 should equal 5")

    def test_add_negative_numbers(self):
        result = add(-1, -2)
        self.assertEqual(result, -3, "-1 + -2 should equal -3")

    def test_add_zero(self):
        result = add(5, 0)
        self.assertEqual(result, 5, "5 + 0 should equal 5")

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

运行测试:python -m unittest test_calculator_unittest.pyunittest.TestCase提供了多种断言方法,如assertEqualassertTrue等,用于验证结果。

2.2 unittest常用断言

  • assertEqual(a, b):检查a == b
  • assertTrue(x):检查x为真。
  • assertRaises(Exception, func, *args):检查函数是否抛出指定异常。

示例:测试抛出异常的场景。

# calculator.py
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# test_calculator_unittest.py
import unittest
from calculator import divide

class TestCalculator(unittest.TestCase):
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

    def test_divide_positive(self):
        result = divide(10, 2)
        self.assertEqual(result, 5.0, "10 / 2 should equal 5.0")

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

3. 使用pytest框架

pytest是一个功能强大且更简洁的测试框架,广泛用于Python项目。它支持更自然的测试编写方式,且有丰富的插件生态。

3.1 安装pytest

运行以下命令安装pytest

pip install pytest

3.2 编写pytest测试

使用相同的calculator.py,以下是pytest风格的测试代码:

# test_calculator_pytest.py
from calculator import add

def test_add_positive_numbers():
    assert add(2, 3) == 5, "2 + 3 should equal 5"

def test_add_negative_numbers():
    assert add(-1, -2) == -3, "-1 + -2 should equal -3"

def test_add_zero():
    assert add(5, 0) == 5, "5 + 0 should equal 5"

运行测试:pytest test_calculator_pytest.pypytest会自动发现以test_开头的文件和函数,执行测试并提供详细的输出。

3.3 测试异常

pytest使用pytest.raises来测试异常:

# test_calculator_pytest.py
import pytest
from calculator import divide

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

def test_divide_positive():
    assert divide(10, 2) == 5.0, "10 / 2 should equal 5.0"

运行:pytest test_calculator_pytest.py

4. 模拟(Mocking)测试

对于依赖外部资源(如数据库、API)的代码,单元测试需要隔离这些依赖。Python的unittest.mock模块提供了模拟功能。

4.1 使用unittest.mock

假设有一个函数调用外部API获取数据:

# api_client.py
import requests

def get_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()

以下是使用mock的测试代码:

# test_api_client.py
import unittest
from unittest.mock import patch
from api_client import get_user_data

class TestApiClient(unittest.TestCase):
    @patch('requests.get')
    def test_get_user_data(self, mock_get):
        mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
        result = get_user_data(1)
        self.assertEqual(result, {"id": 1, "name": "Alice"})
        mock_get.assert_called_once_with("https://api.example.com/users/1")

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

@patch装饰器模拟了requests.get,避免实际的网络请求。

4.2 使用pytestpytest-mock

pytest-mockpytest的插件,简化了模拟操作。安装:pip install pytest-mock

# test_api_client_pytest.py
from api_client import get_user_data

def test_get_user_data(mocker):
    mock_get = mocker.patch('api_client.requests.get')
    mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}

    result = get_user_data(1)
    assert result == {"id": 1, "name": "Alice"}
    mock_get.assert_called_once_with("https://api.example.com/users/1")

运行:pytest test_api_client_pytest.py

5. 测试覆盖率

测试覆盖率用于衡量代码被测试的程度。可以使用pytest-cov插件。

5.1 安装和使用

安装:pip install pytest-cov

运行测试并生成覆盖率报告:

pytest --cov=calculator test_calculator_pytest.py

示例报告:

Name             Stmts   Miss  Cover
------------------------------------
calculator.py        5      0   100%
------------------------------------
TOTAL                5      0   100%

5.2 提高覆盖率

确保测试覆盖所有代码路径,包括异常情况和边缘案例。例如,扩展divide函数的测试:

# test_calculator_pytest.py
import pytest
from calculator import divide

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

def test_divide_positive():
    assert divide(10, 2) == 5.0, "10 / 2 should equal 5.0"

def test_divide_negative():
    assert divide(-10, 2) == -5.0, "-10 / 2 should equal -5.0"

def test_divide_fraction():
    assert divide(1, 2) == 0.5, "1 / 2 should equal 0.5"

6. 测试最佳实践

  • 单一职责:每个测试用例测试一个功能。
  • 命名清晰:测试函数名应描述测试内容,如test_add_negative_numbers
  • 隔离性:测试不应依赖外部状态或彼此。
  • 覆盖边缘情况:测试异常、边界值和特殊输入。
  • 使用TDD:先写测试,再写代码,确保代码满足测试要求。