11Python编程:从入门到实践(第3版)

测试代码

单元测试

unittest测试用例测试方法

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第11章:测试代码(Testing Code)

一、导读

1.1 本章学习目标

第11章介绍Python的单元测试。本章核心目标:

  • 掌握unittest模块:创建测试用例
  • 学会编写测试方法:test_开头的方法
  • 掌握断言方法:assertEqual、assertTrue等
  • 学会使用setUp():测试初始化
  • 理解测试驱动开发(TDD)

1.2 为什么本章如此重要

编写测试是专业程序员的重要技能。测试能提高代码质量和可维护性,防止回归bug。Python的unittest框架提供了完整的测试基础设施。

1.3 核心问题引导

单元测试和集成测试有什么区别?

什么是测试覆盖率?为什么100%覆盖率不等于没有bug?

如何测试抛出异常的代码?

mock对象在测试中如何使用?


二、核心概念详解

11.1 unittest基础

python
import unittest

def format_name(first, last, middle=""):
    """返回格式化后的全名"""
    if middle:
        return f"{first.title()} {middle.title()} {last.title()}"
    return f"{first.title()} {last.title()}"

class NameTestCase(unittest.TestCase):
    """测试format_name函数"""
    
    def test_first_last_name(self):
        """测试只有名和姓的情况"""
        formatted = format_name("janis", "joplin")
        self.assertEqual(formatted, "Janis Joplin")
    
    def test_with_middle_name(self):
        """测试包含中间名的情况"""
        formatted = format_name("john", "hooker", "lee")
        self.assertEqual(formatted, "John Lee Hooker")
    
    def test_empty_middle_name(self):
        """测试空中间名"""
        formatted = format_name("bob", "dylan", "")
        self.assertEqual(formatted, "Bob Dylan")

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

11.2 测试类

python
import unittest

class Survey:
    def __init__(self, question):
        self.question = question
        self.responses = []
    
    def store_response(self, response):
        self.responses.append(response)
    
    def show_results(self):
        for response in self.responses:
            print(f"- {response}")

class TestSurvey(unittest.TestCase):
    def setUp(self):
        """创建测试对象"""
        self.survey = Survey("What language did you first learn?")
    
    def test_store_single_response(self):
        """测试存储单个响应"""
        self.survey.store_response("English")
        self.assertIn("English", self.survey.responses)
    
    def test_store_multiple_responses(self):
        """测试存储多个响应"""
        responses = ["English", "Spanish", "Mandarin"]
        for response in responses:
            self.survey.store_response(response)
        for response in responses:
            self.assertIn(response, self.survey.responses)
    
    def test_empty_survey(self):
        """测试空调查"""
        self.assertEqual(len(self.survey.responses), 0)

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

11.3 常用断言方法

python
class TestAssertions(unittest.TestCase):
    def test_assertions(self):
        # 相等性
        self.assertEqual(1 + 1, 2)
        self.assertNotEqual(1 + 1, 3)
        
        # 布尔值
        self.assertTrue(True)
        self.assertFalse(False)
        
        # 包含
        self.assertIn(1, [1, 2, 3])
        self.assertNotIn(4, [1, 2, 3])
        
        # 类型
        self.assertIsInstance("hello", str)
        self.assertNotIsInstance(42, str)
        
        # 异常
        with self.assertRaises(ValueError):
            int("not a number")
        
        # 近似相等
        self.assertAlmostEqual(0.1 + 0.2, 0.3, places=10)
        
        # 正则匹配
        self.assertRegex("hello world", r"hello \w+")

11.4 pytest简介

python
# test_example.py (pytest风格)
import pytest

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

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

def test_divide():
    assert divide(10, 2) == 5
    with pytest.raises(ValueError):
        divide(10, 0)

@pytest.fixture
def sample_data():
    return [1, 2, 3, 4, 5]

def test_sum(sample_data):
    assert sum(sample_data) == 15

# 参数化测试
@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, -1, -2),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

三、实战案例

3.1 测试一个完整的模块

python
# calculator.py
class Calculator:
    def __init__(self):
        self.history = []
    
    def add(self, a, b):
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Division by zero")
        result = a / b
        self.history.append(f"{a} / {b} = {result}")
        return result
    
    def get_history(self):
        return self.history

# test_calculator.py
import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()
    
    def test_add(self):
        self.assertEqual(self.calc.add(2, 3), 5)
        self.assertEqual(self.calc.add(-1, 1), 0)
    
    def test_divide(self):
        self.assertEqual(self.calc.divide(10, 2), 5)
        self.assertAlmostEqual(self.calc.divide(1, 3), 0.333, places=2)
    
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            self.calc.divide(10, 0)
    
    def test_history(self):
        self.calc.add(1, 2)
        self.calc.divide(10, 5)
        history = self.calc.get_history()
        self.assertEqual(len(history), 2)
        self.assertIn("1 + 2 = 3", history)

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

四、本章小结

第11章介绍了Python的测试:unittest框架、测试用例、断言方法、setUp()、pytest。编写测试是保证代码质量的重要手段。


五、思考与练习

思考题

单元测试和端到端测试有什么区别?

什么是mock?什么时候需要mock?

测试覆盖率100%能保证没有bug吗?

TDD的流程是什么?

编程练习

为之前的函数和类编写完整的单元测试。

使用pytest重写unittest测试。

实现一个测试覆盖率报告工具。


11/11测试代码
0%