3、单元测试基础:单元测试定义、单元测试框架选型(JUnit/Pytest)、第一个单元测试用例

好,咱们今天聊聊单元测试。说实话,很多刚入行的同学觉得单元测试就是「写几个assert」,其实远没那么简单。我见过太多项目,单元测试覆盖率看着挺高,但一上线就崩——为什么?因为测试根本没测到点子上。

3.1 单元测试到底是个啥?

单元测试,说白了就是验证代码里最小的可测试单元。在面向对象里,这个单元通常是一个类或一个方法;在函数式编程里,就是一个函数。它的核心目标就一个:确保这段代码在给定输入下,输出符合预期

我个人习惯把单元测试比作「代码的体检报告」。你想想看,一个大型系统就像一个人体,单元测试就是检查每个器官是否正常工作。如果每个器官都健康,那整个人的健康就有基本保障了。

单元测试的三个核心特征:

  • 隔离性:不依赖外部资源(数据库、网络、文件系统)
  • 可重复性:每次运行结果一致
  • 快速性:毫秒级执行,能频繁运行

我在项目中遇到过最典型的反例:有人把单元测试写成了集成测试。测试一个Service方法,里面调了数据库、发了HTTP请求、还写了文件。结果跑一次测试要30秒,整个团队都不愿意跑。嗯,这其实已经不算单元测试了。

3.2 框架选型:JUnit vs Pytest

选框架这事儿,其实没有绝对的对错。我两个都用过,说说我的感受。

维度 JUnit (Java) Pytest (Python)
语言生态 Java 标配,Spring 全家桶深度集成 Python 社区首选,Flask/Django 都支持
断言风格 Assertions.assertThat() 链式调用 assert 原生语句,简洁直观
参数化测试 @ParameterizedTest + @CsvSource @pytest.mark.parametrize 装饰器
Mock 支持 Mockito / EasyMock unittest.mock / pytest-mock
测试发现 按命名约定自动发现(*Test.java) 按文件名和函数名自动发现(test_*.py)
学习曲线 中等,注解较多 低,Pythonic 风格

我个人建议:Java 项目无脑选 JUnit 5 + Mockito,这是业界标准组合。Python 项目选 Pytest,它比自带的 unittest 好用太多。我曾经在一个 Python 项目里看到有人用 unittest 写了上千行测试代码,后来我花了一天时间全部重构到 Pytest,代码量直接砍半。

避坑指南:我曾经在选型时犯过一个错——为了「统一技术栈」,在 Java 项目里强行用 TestNG。结果发现社区资源少、文档不全,最后又切回 JUnit。所以我的建议是:选主流框架,别标新立异

3.3 第一个单元测试用例

光说不练假把式。咱们直接上手写一个。假设我们有一个简单的计算器类:

Java 版本(JUnit 5)

// 被测试类
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("除数不能为0");
        }
        return a / b;
    }
}

// 测试类
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {
    
    @Test
    void testAdd() {
        Calculator calc = new Calculator();
        int result = calc.add(3, 5);
        assertEquals(8, result, "3 + 5 应该等于 8");
    }
    
    @Test
    void testDivideByZero() {
        Calculator calc = new Calculator();
        assertThrows(IllegalArgumentException.class, 
            () -> calc.divide(10, 0),
            "除数为0应该抛出异常");
    }
}

Python 版本(Pytest)

# 被测试模块
class Calculator:
    def add(self, a, b):
        return a + b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("除数不能为0")
        return a / b

# 测试文件
import pytest

class TestCalculator:
    
    def test_add(self):
        calc = Calculator()
        result = calc.add(3, 5)
        assert result == 8, "3 + 5 应该等于 8"
    
    def test_divide_by_zero(self):
        calc = Calculator()
        with pytest.raises(ValueError, match="除数不能为0"):
            calc.divide(10, 0)

注意:写第一个测试用例时,最容易犯的错是「测试太复杂」。我见过有人第一个测试用例就写了50行,又是Mock又是参数化。其实没必要。第一个测试用例应该遵循「三A原则」:

  • Arrange(准备):创建对象、准备数据
  • Act(执行):调用被测方法
  • Assert(断言):验证结果

保持简单,一个测试只验证一个行为。

3.4 测试用例的命名规范

命名这事儿,看似小事,其实影响很大。我见过最糟糕的命名就是 test1()test2()——跑挂了根本不知道哪个功能出了问题。

我个人推荐两种命名风格:

  1. 方法名_场景_预期结果testAdd_TwoPositiveNumbers_ReturnsSum
  2. should_场景_when_条件should_throwException_when_divideByZero

两种都可以,关键是团队统一。我在上一个项目里强制要求所有测试用例必须用第一种风格,代码审查时看到 test1 直接打回。一开始有人抱怨,但三个月后,所有人都说真香——因为看测试报告时,一眼就知道哪里出了问题。

3.5 运行你的第一个测试

写完了测试,怎么跑?

  • JUnit:IDE 里直接右键运行,或者用 Maven/Gradle 的 mvn test / gradle test
  • Pytest:命令行执行 pytest test_calculator.py -v-v 参数会显示详细输出

我第一次跑单元测试时,看到绿色的「PASSED」标识,心里还挺激动的。但后来发现,真正的挑战不是让测试通过,而是让测试真正有意义。记住:一个能通过的测试,不一定是个好测试;但一个不能通过的测试,一定是个有用的测试

小技巧:刚开始写单元测试时,可以先从「边界条件」入手。比如测试一个方法时,先测正常值、再测边界值(0、负数、最大值)、最后测异常情况。这样能覆盖大部分问题。

好了,单元测试的基础就聊到这儿。下一节咱们会深入讲讲「如何写出高质量的测试用例」,包括测试覆盖率、Mock 技巧、以及如何避免常见的测试反模式。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。