Python单元测试完全指南:从入门到实战

0 阅读6分钟

什么是单元测试?为什么你需要它

单元测试,说白了就是给你的代码写"体检报告"。想象一下,你写了一个函数,但你怎么知道它真的能正常工作呢?靠眼睛看?那可太不靠谱了!

单元测试就像是给每个函数配了个"私人医生",专门检查这个函数是不是健康。每次你修改代码,这个"医生"就会立马告诉你:"嘿,你的函数感冒了!"或者"恭喜,一切正常!"

unittest模块:Python内置的测试利器

Python自带了一个超强的测试框架叫unittest。这家伙不需要额外安装,开箱即用!它的设计灵感来自于Java的JUnit,所以如果你用过Java,会觉得很亲切。

基础结构:TestCase类

每个测试都要继承unittest.TestCase类,这就像是给你的测试穿上了一件"制服",让Python知道:"哦,这是个测试!"


import unittest



class TestBasicMath(unittest.TestCase):

    def test_addition(self):

        result = 2 + 3

        self.assertEqual(result, 5)

    

    def test_subtraction(self):

        result = 10 - 4

        self.assertEqual(result, 6)



if __name__ == '__main__':

    unittest.main()

看到没?测试方法必须以test_开头,这是unittest的规矩!就像进门要敲门一样,不敲门人家不知道你是谁。

断言方法:测试的"判官"

unittest提供了一堆断言方法,它们就像是法官的小锤子,专门用来判断"对"还是"错"。

常用断言方法大集合


class TestAssertions(unittest.TestCase):

    def test_equality(self):

        # 相等性测试

        self.assertEqual(1 + 1, 2)  # 最常用的

        self.assertNotEqual(1, 2)   # 不相等

    

    def test_boolean(self):

        # 布尔值测试

        self.assertTrue(True)       # 必须是True

        self.assertFalse(False)     # 必须是False

    

    def test_none(self):

        # None值测试

        self.assertIsNone(None)     # 必须是None

        self.assertIsNotNone("hello")  # 不能是None

    

    def test_membership(self):

        # 成员关系测试

        self.assertIn(1, [1, 2, 3])    # 1必须在列表中

        self.assertNotIn(4, [1, 2, 3]) # 4不能在列表中

这些断言就像是你的"测试工具箱",不同的场景用不同的工具。用错了工具,测试就不准确!

实战案例:测试一个计算器类

让我们写个实际的例子,测试一个简单的计算器:


# calculator.py

class Calculator:

    def add(self, a, b):

        return a + b

    

    def subtract(self, a, b):

        return a - b

    

    def multiply(self, a, b):

        return a * b

    

    def divide(self, a, b):

        if b == 0:

            raise ValueError("除数不能为零!")

        return a / b



# test_calculator.py

import unittest

from calculator import Calculator



class TestCalculator(unittest.TestCase):

    def setUp(self):

        # 每个测试方法运行前都会执行

        self.calc = Calculator()

    

    def test_add(self):

        result = self.calc.add(3, 5)

        self.assertEqual(result, 8)

        

        # 测试负数

        result = self.calc.add(-1, 1)

        self.assertEqual(result, 0)

    

    def test_subtract(self):

        result = self.calc.subtract(10, 3)

        self.assertEqual(result, 7)

    

    def test_multiply(self):

        result = self.calc.multiply(4, 5)

        self.assertEqual(result, 20)

        

        # 测试零乘法

        result = self.calc.multiply(0, 100)

        self.assertEqual(result, 0)

    

    def test_divide(self):

        result = self.calc.divide(10, 2)

        self.assertEqual(result, 5.0)

        

        # 测试除零异常(超级重要!)

        with self.assertRaises(ValueError):

            self.calc.divide(10, 0)



if __name__ == '__main__':

    unittest.main()

setUp和tearDown:测试的"准备"和"收尾"工作

有时候,每个测试都需要做同样的准备工作,比如创建对象、连接数据库等等。setUp就是干这个的!


class TestWithSetup(unittest.TestCase):

    def setUp(self):

        # 每个测试方法前都会运行

        print("准备测试环境...")

        self.data = [1, 2, 3, 4, 5]

    

    def tearDown(self):

        # 每个测试方法后都会运行

        print("清理测试环境...")

        self.data = None

    

    def test_data_length(self):

        self.assertEqual(len(self.data), 5)

    

    def test_data_content(self):

        self.assertIn(3, self.data)

setUp就像是测试前的"热身运动",tearDown就像是测试后的"整理运动"。做得好,测试就稳定!

测试异常:期待"出错"的情况

有时候,我们的代码应该抛出异常。比如除零、文件不存在等等。这时候用assertRaises


class TestExceptions(unittest.TestCase):

    def test_division_by_zero(self):

        with self.assertRaises(ZeroDivisionError):

            result = 10 / 0

    

    def test_key_error(self):

        data = {"a": 1, "b": 2}

        with self.assertRaises(KeyError):

            value = data["c"]  # 这个键不存在,应该报错

    

    def test_custom_exception(self):

        def risky_function():

            raise ValueError("这是个自定义错误")

        

        with self.assertRaises(ValueError) as context:

            risky_function()

        

        # 还可以检查错误消息

        self.assertEqual(str(context.exception), "这是个自定义错误")

跳过测试:有时候"逃避"是明智的

不是所有测试都要每次都跑。有时候某些功能还没实现,或者在特定环境下才能运行:


class TestWithSkips(unittest.TestCase):

    @unittest.skip("这个功能还没实现")

    def test_future_feature(self):

        # 这个测试会被跳过

        pass

    

    @unittest.skipIf(sys.platform == "win32", "Windows下不运行")

    def test_unix_only(self):

        # Windows系统会跳过这个测试

        pass

    

    @unittest.skipUnless(sys.platform.startswith("linux"), "只在Linux下运行")

    def test_linux_only(self):

        # 只有Linux系统才会运行这个测试

        pass

运行测试:多种方式任你选

命令行运行

最简单的方式:


# 运行单个文件

python test_calculator.py



# 运行所有测试

python -m unittest discover



# 运行特定的测试类

python -m unittest test_calculator.TestCalculator



# 运行特定的测试方法

python -m unittest test_calculator.TestCalculator.test_add

在代码中运行


if __name__ == '__main__':

    # 基本运行

    unittest.main()

    

    # 或者更详细的控制

    suite = unittest.TestLoader().loadTestsFromTestCase(TestCalculator)

    runner = unittest.TextTestRunner(verbosity=2)

    runner.run(suite)

测试覆盖率:你的代码有多少被测试了?

写了测试,但你怎么知道测试得够不够全面?这时候需要coverage工具:


# 安装coverage

pip install coverage



# 运行测试并收集覆盖率信息

coverage run -m unittest discover



# 查看覆盖率报告

coverage report



# 生成HTML报告

coverage html

覆盖率就像是"测试地图",告诉你哪些代码走过了,哪些还是"未知领域"。

最佳实践:写好测试的秘诀

1. 测试命名要清晰

不要写test_1test_2这种无意义的名字!要写成test_addition_with_positive_numbers这样一看就懂的。

2. 一个测试只测一件事

不要在一个测试方法里塞太多东西,就像吃饭不要狼吞虎咽一样。

3. 测试要独立

每个测试都应该能独立运行,不依赖其他测试的结果。想象每个测试都是独立的个体!

4. 测试边界条件

别只测试"正常"情况,要测试边界:空列表、零值、超大数字等等。这些往往是bug的温床!


class TestBoundaryConditions(unittest.TestCase):

    def test_empty_list(self):

        result = sum([])  # 空列表求和

        self.assertEqual(result, 0)

    

    def test_single_element(self):

        result = max([42])  # 单元素列表求最大值

        self.assertEqual(result, 42)

    

    def test_large_numbers(self):

        result = 999999 + 1

        self.assertEqual(result, 1000000)

总结:测试让代码更自信

单元测试不是负担,而是给你的代码买了份"保险"。每次修改代码,测试就会告诉你:"放心,没问题!"或者"等等,这里有问题!"

记住几个关键点:

  • 测试要简单明了,一看就懂

  • 覆盖各种情况,特别是边界条件

  • 测试之间要独立,不能相互依赖

  • 命名要清晰,维护的时候不会迷糊

unittest虽然不是最时髦的测试框架,但它稳定、可靠、功能全面。就像老式的锤子一样,也许不够花哨,但绝对能把钉子敲进去!

开始给你的代码写测试吧,你会发现编程变得更有信心,bug变得更少,睡觉也更香甜了!