什么是 JUnit ?
JUnit 是Java 单元测试框架最常用的框架之一。JUnit是一个用于编写和运行测试的框架,简单易学。每个测试都是一个方法,包含特定场景下将执行的部分代码。比较预期输出和实际输出,以实现代码验证。
JUnit 简单实例
测试类通常包含三个阶段:
1. 准备;
2. 测试;
3. 清理
下面列举了一个单元测试的小案例:
import org.junit.*; import java.util.Arrays; import java.util.List; public class FriendshipsTest { Friendships friendships; //一、 测试准备数据的方法 @BeforeClass public static void beforeClass() { // @BeforeClass 指定的方法仅在执行类中的测试方法前执行一次 // 非常适合用于执行大部分乃至全部测试都要求的一般性准备工作。 } //一、 测试准备数据的方法 @Before public void before() { // @Before 指定的方法将在每个测试方法前运行。 friendships = new Friendships(); friendships.makeFriends("Joe", "Audrey"); friendships.makeFriends("Joe", "Peter"); friendships.makeFriends("Joe", "Michael"); friendships.makeFriends("Joe", "Britney"); friendships.makeFriends("Joe", "Paul"); } //二、 执行实际测试的方法 @Test public void alexDoesNotHaveFriends() { Assert.assertTrue("Alex does not have friends", friendships.getFriendsList("Alex").isEmpty()); } //二、 执行实际测试的方法 @Test public void joeHas5Friends() { Assert.assertEquals("Joe has 5 friends", 5, friendships.getFriendsList("Joe").size()); } //二、 执行实际测试的方法 @Test public void joeIsFriendWithEveryone() { List<String> friendsOfJoe = Arrays.asList("Audrey", "Peter", "Michael", "Britney", "Paul"); Assert.assertTrue( friendships.getFriendsList("Joe").containsAll(friendsOfJoe) ); } //三、 执行完成后清理现场的方法 @AfterClass public static void afterClass() { // @AfterClass 指定的方法仅在执行类中的测试方法后执行一次 } //三、 执行完成后清理现场的方法 @After public void after() { // @After 指定的方法将在每个测试方法后运行。 } }
要测试的逻辑代码:
import java.util.*; public class Friendships { private final Map<String, List<String>> friendships = new HashMap<>(); public void makeFriends(String person1, String person2) { addFriend(person1, person2); addFriend(person2, person1); } public List<String> getFriendsList(String person) { if (!friendships.containsKey(person)) { return Collections.emptyList(); } return friendships.get(person); } public boolean areFriends(String person1, String person2) { return friendships.containsKey(person1) && friendships.get(person1).contains(person2); } private void addFriend(String person, String friend) { if (!friendships.containsKey(person)) { friendships.put(person, new ArrayList<String>()); } List<String> friends = friendships.get(person); if (!friends.contains(friend)) { friends.add(friend); } } }
单元测试旨在对一小块代码进行测试。
最佳实践
命名约定
1. 命名约定有助于更好地组织测试,从而让开发人员更容易测试。另一个好处是,很多工具都遵守这些约定(不遵守的话工具可能没办法执行输出正常的结果)。
2. 将实现代码(src/main/java)和测试代码 (src/test/java)分开,好处避免不小心将测试和产品二级制文件一起打包,很多构建工具都明确要求测试位于特定的源代码目录;
3. 测试类和实现类放在同一个包中。有助于更快找到代码。
4. 以类似于受测类的方式给测试类命名。有助于快速找到测试类。
5. 给测试方法指定描述性名称。有助于明白测试的目标。
流程
1. 先编写测试再写实现代码;
2. 仅在测试失败后才编写新代码;
3. 每次修改实现代码后,都再次运行所有测试;
4. 仅当所有测试都通过后才编写新测试;
5. 仅当测试都通过后才重构;
开发实践
1. 编写让测试能够通过的最简单的代码;
2. 为了更早的澄清测试目的,先编写断言,在编写操作;
3. 最大限度减少每个测试中的断言;
4. 不要让测试依赖其他测试;
5. 测试的运行速度必须快;
6. 使用测试替身,好处减少代码依赖并提高测试的执行速度;
7. 使用 set-up 和 tear-down 语义的方法;
8. 让测试更清晰不要在测试类中使用基类;
工具
1. 确保测试覆盖每个角落,使用代码覆盖率和持续集成(CI)工具;
2. 测试驱动开发(TDD) 和 行为驱动开发(BDD)结合使用;