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 连接,避免了大量重复代码。
@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。这笔账,你算算看?