4. 单元测试与代码质量:JUnit、pytest、SonarQube

单元测试这件事,我早年其实挺不以为然的。总觉得「代码能跑就行,写测试多浪费时间」。直到有一次,我负责的一个支付模块上线后出了严重问题——一个金额计算的小数点精度错误,导致线上对账差了十几万。嗯,从那以后,我再也不敢不写单元测试了。

说白了,单元测试就是给你的代码上保险。你想想看,一个项目几百个类、几千个方法,改一行代码谁知道会影响到哪里?单元测试就是帮你兜底的。

4.1 Java 生态:JUnit 5 实战

JUnit 5 是目前 Java 生态里最主流的测试框架。我个人习惯用它的参数化测试和断言机制,写起来很顺手。

先看一个最简单的例子:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    @Test
    void testAddition() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals(5, result, "2 + 3 应该等于 5");
    }
}

这里要注意,assertEquals 的第三个参数是失败时的提示信息。我建议你每次都写上,不然测试失败了你还得去翻代码猜意图。

我的习惯:每个测试方法只测一个逻辑点。如果一个方法里写了三个断言,那说明这个测试太「胖」了,拆开更好。

参数化测试是我特别喜欢的功能。比如你要测试一个判断闰年的方法,手动写十几个测试用例太累了:

@ParameterizedTest
@CsvSource({
    "2020, true",
    "2021, false",
    "1900, false",
    "2000, true"
})
void testIsLeapYear(int year, boolean expected) {
    assertEquals(expected, DateUtils.isLeapYear(year));
}

这样写,数据驱动,一目了然。我曾经在一个金融项目中,用参数化测试覆盖了 200 多个边界条件,跑一遍只要 3 秒。

4.2 Python 生态:pytest 实战

如果你做 Python 项目,pytest 几乎是标配。它比 unittest 简洁太多了。

同样的加法测试,pytest 写出来是这样的:

# test_calculator.py
def test_addition():
    calc = Calculator()
    assert calc.add(2, 3) == 5

就一行 assert,干净利落。pytest 会自动发现以 test_ 开头的函数并执行。

fixture 是 pytest 的核心功能之一。比如你要测试数据库操作,每次都要初始化连接:

import pytest

@pytest.fixture
def db_connection():
    conn = create_database_connection()
    yield conn
    conn.close()

def test_user_query(db_connection):
    user = db_connection.query("SELECT * FROM users WHERE id=1")
    assert user.name == "Alice"

这里 yield 之前的代码是「前置准备」,之后的代码是「后置清理」。我曾经在项目中用 fixture 管理 Redis 连接,避免了大量重复代码。

避坑指南:fixture 的作用域默认是 function 级别的。如果你有耗时的初始化操作(比如启动一个 Docker 容器),记得加上 @pytest.fixture(scope="session"),否则每个测试函数都会重新执行一次,慢得你想哭。

4.3 测试覆盖率门禁

测试覆盖率,说白了就是「你的代码有多少行被测试执行过」。但我要说一句:覆盖率不是越高越好。

我曾经见过一个团队,硬性要求覆盖率必须达到 90%。结果呢?开发人员开始写「凑数测试」——测试里全是空断言,或者只测 getter/setter。这种测试除了让数字好看,没有任何价值。

我个人建议的做法是:

  • 核心业务逻辑:覆盖率要求 85% 以上
  • 工具类、配置类:覆盖率可以放宽到 60%
  • 自动生成的代码:直接排除在统计之外

在 CI 流水线中,我们可以设置覆盖率门禁。比如用 JaCoCo(Java)或 pytest-cov(Python):

# Java 项目(pom.xml)
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <configuration>
        <rules>
            <rule>
                <element>CLASS</element>
                <limits>
                    <limit>
                        <counter>LINE</counter>
                        <value>COVEREDRATIO</value>
                        <minimum>0.80</minimum>
                    </limit>
                </limits>
            </rule>
        </rules>
    </configuration>
</plugin>
# Python 项目
# 在 pytest 命令中加上 --cov-fail-under=80
pytest --cov=myapp --cov-fail-under=80 tests/

当覆盖率低于 80% 时,CI 构建直接失败。这样能倒逼团队重视测试质量。

4.4 静态代码分析:SonarQube

静态代码分析,说白了就是「不用运行代码,光看源码就能找出问题」。SonarQube 是目前最主流的工具。

它能检测出什么问题呢?我列几个常见的:

问题类型 示例 严重程度
代码异味 方法过长(超过 30 行) 轻微
潜在 Bug 空指针未检查 严重
安全漏洞 SQL 注入风险 致命
重复代码 相同逻辑出现 3 次以上 中等

在 CI 中集成 SonarQube 很简单。以 Jenkins 为例,你只需要在构建后加一个 SonarQube Scanner 步骤:

stage('SonarQube Analysis') {
    steps {
        withSonarQubeEnv('SonarQube') {
            sh 'mvn sonar:sonar'
        }
    }
}

扫描完成后,SonarQube 会生成一份质量报告。我建议你设置一个「质量门禁」(Quality Gate):

  • 新增代码的覆盖率不低于 80%
  • 没有 blocker 级别的 Bug
  • 代码重复率不超过 5%

如果门禁没通过,CI 构建直接失败。这样能保证每次合并的代码都是「干净」的。

核心要点:单元测试 + 覆盖率门禁 + 静态代码分析,这三者缺一不可。测试保证「功能正确」,覆盖率保证「测试充分」,静态分析保证「代码健康」。三管齐下,才能构建出高质量的软件。

4.5 我的实战建议

最后,分享几个我在项目中踩过的坑:

  • 不要追求 100% 覆盖率。我曾经见过一个项目,为了凑 100% 覆盖率,连 main 方法都写了测试,纯属浪费生命。
  • 测试也要维护。业务代码改了,测试代码也要同步更新。否则测试就是一堆「绿色废纸」。
  • SonarQube 的规则可以自定义。默认规则太严格了,比如「方法不能超过 15 行」这种,在业务代码里很难做到。我一般会放宽到 30 行。

嗯,单元测试这件事,说到底是一种「投资」。前期多花点时间写测试,后期就能少熬夜修 Bug。这笔账,你算算看?