💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
#### [JUnit](https://lingcoder.gitee.io/onjava8/#/book/16-Validating-Your-Code?id=junit) 最初的 JUnit 发布于 2000 年,大概是基于 Java 1.0,因此不能使用 Java 的反射工具。因此,用旧的 JUnit 编写单元测试是一项相当繁忙和冗长的工作。我发现这个设计令人不爽,并编写了自己的单元测试框架作为[注解](https://lingcoder.gitee.io/onjava8/#/./Annotations)一章的示例。这个框架走向了另一个极端,“尝试最简单可行的方法”(极限编程中的一个关键短语)。从那之后,JUnit 通过反射和注解得到了极大的改进,大大简化了编写单元测试代码的过程。在 Java8 中,他们甚至增加了对 lambdas 表达式的支持。本书使用当时最新的 Junit5 版本 在 JUnit 最简单的使用中,使用**@Test**注解标记表示测试的每个方法。JUnit 将这些方法标识为单独的测试,并一次设置和运行一个测试,采取措施避免测试之间的副作用。 让我们尝试一个简单的例子。**CountedList**继承**ArrayList**,添加信息来追踪有多少个**CountedLists**被创建: ~~~ // validating/CountedList.java // Keeps track of how many of itself are created. package validating; import java.util.*; public class CountedList extends ArrayList<String> { private static int counter = 0; private int id = counter++; public CountedList() { System.out.println("CountedList #" + id); } public int getId() { return id; } } ~~~ 标准做法是将测试放在它们自己的子目录中。测试还必须放在包中,以便 JUnit 能发现它们: ~~~ // validating/tests/CountedListTest.java // Simple use of JUnit to test CountedList. package validating; import java.util.*; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; public class CountedListTest { private CountedList list; @BeforeAll static void beforeAllMsg() { System.out.println(">>> Starting CountedListTest"); } @AfterAll static void afterAllMsg() { System.out.println(">>> Finished CountedListTest"); } @BeforeEach public void initialize() { list = new CountedList(); System.out.println("Set up for " + list.getId()); for(int i = 0; i < 3; i++) list.add(Integer.toString(i)); } @AfterEach public void cleanup() { System.out.println("Cleaning up " + list.getId()); } @Test public void insert() { System.out.println("Running testInsert()"); assertEquals(list.size(), 3); list.add(1, "Insert"); assertEquals(list.size(), 4); assertEquals(list.get(1), "Insert"); } @Test public void replace() { System.out.println("Running testReplace()"); assertEquals(list.size(), 3); list.set(1, "Replace"); assertEquals(list.size(), 3); assertEquals(list.get(1), "Replace"); } // A helper method to simplify the code. As // long as it's not annotated with @Test, it will // not be automatically executed by JUnit. private void compare(List<String> lst, String[] strs) { assertArrayEquals(lst.toArray(new String[0]), strs); } @Test public void order() { System.out.println("Running testOrder()"); compare(list, new String[] { "0", "1", "2" }); } @Test public void remove() { System.out.println("Running testRemove()"); assertEquals(list.size(), 3); list.remove(1); assertEquals(list.size(), 2); compare(list, new String[] { "0", "2" }); } @Test public void addAll() { System.out.println("Running testAddAll()"); list.addAll(Arrays.asList(new String[] { "An", "African", "Swallow"})); assertEquals(list.size(), 6); compare(list, new String[] { "0", "1", "2", "An", "African", "Swallow" }); } } /* Output: >>> Starting CountedListTest CountedList #0 Set up for 0 Running testRemove() Cleaning up 0 CountedList #1 Set up for 1 Running testReplace() Cleaning up 1 CountedList #2 Set up for 2 Running testAddAll() Cleaning up 2 CountedList #3 Set up for 3 Running testInsert() Cleaning up 3 CountedList #4 Set up for 4 Running testOrder() Cleaning up 4 >>> Finished CountedListTest */ ~~~ **@BeforeAll**注解是在任何其他测试操作之前运行一次的方法。**@AfterAll**是所有其他测试操作之后只运行一次的方法。两个方法都必须是静态的。 **@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。可以将所有这样的初始化放在测试类的构造函数中,尽管我认为**@BeforeEach**更加清晰。JUnit为每个测试创建一个对象,确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用**@BeforeEach**和构造函数之间的唯一区别是**@BeforeEach**在测试前直接调用。在大多数情况下,这不是问题,如果你愿意,可以使用构造函数方法。 如果你必须在每次测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc),那就用注解**@AfterEach**。 每个测试创建一个新的**CountedListTest**对象,任何非静态成员变量也会在同一时间创建。然后为每个测试调用**initialize()**,于是 list 被赋值为一个新的用字符串“0”、“1” 和 “2” 初始化的**CountedList**对象。观察**@BeforeEach**和**@AfterEach**的行为,这些方法在初始化和清理测试时显示有关测试的信息。 **insert()**和**replace()**演示了典型的测试方法。JUnit 使用**@Test**注解发现这些方法,并将每个方法作为测试运行。在方法内部,你可以执行任何所需的操作并使用 JUnit 断言方法(以"assert"开头)验证测试的正确性(更全面的"assert"说明可以在 Junit 文档里找到)。如果断言失败,将显示导致失败的表达式和值。这通常就足够了,但是你也可以使用每个 JUnit 断言语句的重载版本,它包含一个字符串,以便在断言失败时显示。 断言语句不是必须的;你可以在没有断言的情况下运行测试,如果没有异常,则认为测试是成功的。 **compare()**是“helper方法”的一个例子,它不是由 JUnit 执行的,而是被类中的其他测试使用。只要没有**@Test**注解,JUnit 就不会运行它,也不需要特定的签名。在这里,**compare()**是私有方法 ,表示仅在测试类中使用,但他同样可以是**public**。其余的测试方法通过将其重构为**compare()**方法来消除重复的代码。 本书使用**build.gradle**控制测试,运行本章节的测试,使用命令:`gradlew validating:test`,Gradle 不会运行已经运行过的测试,所以如果你没有得到测试结果,得先运行:`gradlew validating:clean`。 可以用下面这个命令运行本书的所有测试: **gradlew test** 尽管可以用最简单的方法,如**CountedListTest.java**所示那样,JUnit 还包括大量的测试结构,你可以到[官网](https://lingcoder.gitee.io/onjava8/#/junit.org)上学习它们。 JUnit 是 Java 最流行的单元测试框架,但也有其它可以替代的。你可以通过互联网发现更适合的那一个。