你好呀,我是 JUnit,一个开源的 Java 单元测试框架。在了解我以前,先来了解一下什么是单元测试。单元测试,就是针对最小的功能单元编写测试代码。在 Java 中,最小的功能单元就是方法,所以,对 Java 程序员进行单元测试实际上就是对 Java 方法的测试。java
为何要进行单元测试呢?由于单元测试能够确保你编写的代码是符合软件需求和遵循开发规范的。单元测试是全部测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是惟一一次可以达到代码覆盖率 100% 的测试,是整个软件测试过程的基础和前提。能够这么说,单元测试的性价比是最好的。程序员
微软公司以前有这样一个统计:bug 在单元测试阶段被发现的平均耗时是 3.25 小时,若是遗漏到系统测试则须要 11.5 个小时。编程
经我这么一说,你应该已经很清楚单元测试的重要性了。那在你最初编写测试代码的时候,是否是常常这么作?就像下面这样。markdown
public class Factorial {
public static long fact(long n) {
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
public static void main(String[] args) {
if (fact(3) == 6) {
System.out.println("经过");
} else {
System.out.println("失败");
}
}
}
复制代码
要测试 fact()
方法正确性,你在 main()
方法中编写了一段测试代码。若是你这么作过的话,我只能说你也曾经青涩天真过啊!使用 main()
方法来测试有不少坏处,好比说:框架
1)测试代码没有和源代码分开。编辑器
2)不够灵活,很难编写一组通用的测试代码。ide
3)没法自动打印出预期和实际的结果,没办法比对。post
但若是学会使用我——JUnit 的话,就不会再有这种困扰了。我能够很是简单地组织测试代码,并随时运行它们,还能给出准确的测试报告,让你在最短的时间内发现本身编写的代码到底哪里出了问题。单元测试
好了,既然知道了我这么优秀,那还等什么,直接上手吧!我最新的版本是 JUnit 5,Intellij IDEA 中已经集成了,因此你能够直接在 IDEA 中编写并运行个人测试用例。测试
第一步,直接在当前的代码编辑器窗口中按下 Command+N
键(Mac 版),在弹出的菜单中选择「Test...」。
勾选上要编写测试用例的方法 fact()
,而后点击「OK」。
此时,IDEA 会自动在当前类所在的包下生成一个类名带 Test(惯例)的测试类。以下图所示。
若是你是第一次使用个人话,IDEA 会提示你导入个人依赖包。建议你选择最新的 JUnit 5.4。
导入完毕后,你能够打开 pom.xml 文件确认一下,里面多了对个人依赖。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
复制代码
第二步,在测试方法中添加一组断言,以下所示。
@Test
void fact() {
assertEquals(1, Factorial.fact(1));
assertEquals(2, Factorial.fact(2));
assertEquals(6, Factorial.fact(3));
assertEquals(100, Factorial.fact(5));
}
复制代码
@Test
注解是我要求的,我会把带有 @Test
的方法识别为测试方法。在测试方法内部,你可使用 assertEquals()
对指望的值和实际的值进行比对。
第三步,你能够在邮件菜单中选择「Run FactorialTest」来运行测试用例,结果以下所示。
测试失败了,由于第 20 行的预期结果和实际不符,预期是 100,实际是 120。此时,你要么修正实现代码,要么修正测试代码,直到测试经过为止。
不难吧?单元测试能够确保单个方法按照正确的预期运行,若是你修改了某个方法的代码,只需确保其对应的单元测试经过,便可认为改动是没有问题的。
在一个测试用例中,可能要对多个方法进行测试。在测试以前呢,须要准备一些条件,好比说建立对象;在测试完成后呢,须要把这些对象销毁掉以释放资源。若是在多个测试方法中重复这些样板代码又会显得很是啰嗦。
这时候,该怎么办呢?
我为你提供了 setUp()
和 tearDown()
,做为一个文化人,我称之为“瞻前顾后”。来看要测试的代码。
public class Calculator {
public int sub(int a, int b) {
return a - b;
}
public int add(int a, int b) {
return a + b;
}
}
复制代码
新建测试用例的时候记得勾选setUp
和 tearDown
。
生成后的代码以下所示。
class CalculatorTest {
Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@AfterEach
void tearDown() {
calculator = null;
}
@Test
void sub() {
assertEquals(0,calculator.sub(1,1));
}
@Test
void add() {
assertEquals(2,calculator.add(1,1));
}
}
复制代码
@BeforeEach
的 setUp()
方法会在运行每一个 @Test
方法以前运行;@AfterEach
的 tearDown()
方法会在运行每一个 @Test
方法以后运行。
与之对应的还有 @BeforeAll
和 @AfterAll
,与 @BeforeEach
和 @AfterEach
不一样的是,All 一般用来初始化和销毁静态变量。
public class DatabaseTest {
static Database db;
@BeforeAll
public static void init() {
db = createDb(...);
}
@AfterAll
public static void drop() {
...
}
}
复制代码
对于 Java 程序来讲,异常处理也很是的重要。对于可能抛出的异常进行测试,自己也是测试的一个重要环节。
还拿以前的 Factorial 类来进行说明。在 fact()
方法的一开始,对参数 n 进行了校验,若是小于 0,则抛出 IllegalArgumentException 异常。
public class Factorial {
public static long fact(long n) {
if (n < 0) {
throw new IllegalArgumentException("参数不能小于 0");
}
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
}
复制代码
在 FactorialTest 中追加一个测试方法 factIllegalArgument()
。
@Test
void factIllegalArgument() {
assertThrows(IllegalArgumentException.class, new Executable() {
@Override
public void execute() throws Throwable {
Factorial.fact(-2);
}
});
}
复制代码
我为你提供了一个 assertThrows()
的方法,第一个参数是异常的类型,第二个参数 Executable,能够封装产生异常的代码。若是以为匿名内部类写起来比较复杂的话,可使用 Lambda 表达式。
@Test
void factIllegalArgumentLambda() {
assertThrows(IllegalArgumentException.class, () -> {
Factorial.fact(-2);
});
}
复制代码
有时候,因为某些缘由,某些方法产生了 bug,须要一段时间去修复,在修复以前,该方法对应的测试用例一直是以失败了结的,为了不这种状况,我为你提供了 @Disabled
注解。
class DisabledTestsDemo {
@Disabled("该测试用例再也不执行,直到编号为 43 的 bug 修复掉")
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
复制代码
@Disabled
注解也能够不须要说明,但我建议你仍是提供一下,简单地说明一下为何这个测试方法要忽略。在上例中,若是团队的其余成员看到说明就会明白,当编号 43 的 bug 修复后,该测试方法会从新启用的。即使是为了提醒本身,也颇有必要,由于时间长了你可能本身就忘了,当初是为何要忽略这个测试方法的。
有时候,你可能须要在某些条件下运行测试方法,有些条件下不运行测试方法。针对这场使用场景,我为你提供了条件测试。
1)不一样的操做系统,可能须要不一样的测试用例,好比说 Linux 和 Windows 的路径名是不同的,经过 @EnabledOnOs
注解就能够针对不一样的操做系统启用不一样的测试用例。
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}
@TestOnMac
void testOnMac() {
// ...
}
@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}
复制代码
2)不一样的 Java 运行环境,可能也须要不一样的测试用例。@EnabledOnJre
和 @EnabledForJreRange
注解就能够知足这个需求。
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
// ...
}
复制代码
最后,给你说三句内心话吧。在编写单元测试的时候,你最好这样作:
1)单元测试的代码自己必须很是名单明了,能一下看明白,决不能再为测试代码编写测试代码。
2)每一个单元测试应该互相独立,不依赖运行时的顺序。
3)测试时要特别注意边界条件,好比说 0,null
,空字符串"" 等状况。
但愿我能尽早的替你发现代码中的 bug,毕竟越早的发现,形成的损失就会越小。see you!
推荐阅读:
太赞了,GitHub 上标星 115k+ 的 Java 教程!
最后的一点点请求,若是这篇文章的确帮助到了你,就顺手点个赞吧,让 2021 年第一次更文的我,快乐那么一下下,谢谢你的鼓励呀,新年咱们一块儿加油!