Java基础——单元测试

132 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

使用Fixture

在测试的时候,我们经常遇到一个对象需要初始化,测试完可能还需要清理的情况。如果每个@Test方法都写一遍这样的重复代码,显然比较麻烦。

JUnit提供了编写测试前准备、测试后清理的固定代码,我们称之为Fixture。

我们要先初始化对象,我们不必在每个测试方法中都写上初始化代码,而是通过@BeforeEach来初始化,通过@AfterEach来清理资源

 public class Calculator {
     private long n = 0;
 ​
     public long add(long x) {
         n = n + x;
         return n;
     }
 ​
     public long sub(long x) {
         n = n - x;
         return n;
     }
 }
 ​
 public class CalculatorTest {
     Calculator calculator;
 ​
     @BeforeEach
     public void setUp() {
         this.calculator = new Calculator();
     }
 ​
     @AfterEach
     public void tearDown() {
         this.calculator = null;
     }
 ​
     @Test
     void testAdd() {
         assertEquals(100, this.calculator.add(100));
         assertEquals(150, this.calculator.add(50));
         assertEquals(130, this.calculator.add(-20));
     }
 ​
     @Test
     void testSub() {
         assertEquals(-100, this.calculator.sub(100));
         assertEquals(-150, this.calculator.sub(50));
         assertEquals(-130, this.calculator.sub(-20));
     }
 }

CalculatorTest测试中,有两个标记为@BeforeEach@AfterEach的方法,它们会在运行每个@Test方法前后自动运行。

还有一些资源初始化和清理可能更加繁琐,而且会耗费较长的时间,例如初始化数据库。JUnit还提供了@BeforeAll@AfterAll,它们在运行所有@Test前后运行,顺序如下:

 invokeBeforeAll(CalculatorTest.class);
 for (Method testMethod : findTestMethods(CalculatorTest.class)) {
     var test = new CalculatorTest(); // 创建Test实例
     invokeBeforeEach(test);
         invokeTestMethod(test, testMethod);
     invokeAfterEach(test);
 }
 invokeAfterAll(CalculatorTest.class);

因为@BeforeAll@AfterAll在所有@Test方法运行前后仅运行一次,因此,它们只能初始化静态变量

 public class DatabaseTest {
     static Database db;
 ​
     @BeforeAll
     public static void initDatabase() {
         db = createDb(...);
     }
     
     @AfterAll
     public static void dropDatabase() {
         ...
     }
 }

总结:

  1. 对于实例变量,在@BeforeEach中初始化,在@AfterEach中清理,它们在各个@Test方法中互不影响,因为是不同的实例。
  2. 对于静态变量,在@BeforeAll中初始化,在@AfterAll中清理,它们在各个@Test方法中均是唯一实例,会影响各个@Test方法。
  3. 每次运行一个@Test方法前,JUnit首先创建一个XxxTest实例,因此,每个@Test方法内部的成员变量都是独立的,不能也无法把成员变量的状态从一个@Test方法带到另一个@Test方法。

异常测试

 @Test
 void testNegative() {
     assertThrows(IllegalArgumentException.class, new Executable() {
         @Override
         public void execute() throws Throwable {
             Factorial.fact(-1);
         }
     });
 }

JUnit提供assertThrows()来期望捕获一个指定的异常。第二个参数Executable封装了我们要执行的会产生异常的代码。当我们执行Factorial.fact(-1)时,必定抛出IllegalArgumentExceptionassertThrows()在捕获到指定异常时表示通过测试,未捕获到异常,或者捕获到的异常类型不对,均表示测试失败。

有些童鞋会觉得编写一个Executable的匿名类太繁琐了。实际上,Java 8开始引入了函数式编程,所有单方法接口都可以简写如下:

 @Test
 void testNegative() {
     assertThrows(IllegalArgumentException.class, () -> {
         Factorial.fact(-1);
     });
 }

条件测试

排出某些@Test方法,不要让它运行

 @Disabled
 @Test
 void testBug101() {
     // 这个测试不会运行
 }
 public class Config {
     public String getConfigFile(String filename) {
         String os = System.getProperty("os.name").toLowerCase();
         if (os.contains("win")) {
             return "C:\" + filename;
         }
         if (os.contains("mac") || os.contains("linux") || os.contains("unix")) {
             return "/usr/local/" + filename;
         }
         throw new UnsupportedOperationException();
     }
 }
 ​
 @Test
 @EnabledOnOs(OS.WINDOWS)
 void testWindows() {
     assertEquals("C:\test.ini", config.getConfigFile("test.ini"));
 }
 ​
 @Test
 @EnabledOnOs({ OS.LINUX, OS.MAC })
 void testLinuxAndMac() {
     assertEquals("/usr/local/test.cfg", config.getConfigFile("test.cfg"));
 }

不在Windows平台执行的测试,可以加上@DisabledOnOs(OS.WINDOWS)

 @Test
 @DisabledOnOs(OS.WINDOWS)
 void testOnNonWindowsOs() {
     // TODO: this test is disabled on windows
 }

只能在Java 9或更高版本执行的测试,可以加上@DisabledOnJre(JRE.JAVA_8)

 @Test
 @DisabledOnJre(JRE.JAVA_8)
 void testOnJava9OrAbove() {
     // TODO: this test is disabled on java 8
 }

只能在64位操作系统上执行的测试,可以用@EnabledIfSystemProperty判断:

 @Test
 @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
 void testOnlyOn64bitSystem() {
     // TODO: this test is only run on 64 bit system
 }

需要传入环境变量DEBUG=true才能执行的测试,可以用@EnabledIfEnvironmentVariable

 @Test
 @EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")
 void testOnlyOnDebugMode() {
     // TODO: this test is only run on DEBUG=true
 }

参数化测试

JUnit提供了一个@ParameterizedTest注解,用来进行参数化测试。

 @ParameterizedTest
 @ValueSource(ints = { 0, 1, 5, 100 })
 void testAbs(int x) {
     assertEquals(x, Math.abs(x));
 }
 ​
 @ParameterizedTest
 @ValueSource(ints = { -1, -5, -100 })
 void testAbsNegative(int x) {
     assertEquals(-x, Math.abs(x));
 }
 public class StringUtils {
     public static String capitalize(String s) {
         if (s.length() == 0) {
             return s;
         }
         return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
     }
 }
 ​
 @ParameterizedTest
 @MethodSource
 void testCapitalize(String input, String result) {
     assertEquals(result, StringUtils.capitalize(input));
 }
 ​
 static List<Arguments> testCapitalize() {
     return List.of( // arguments:
             Arguments.of("abc", "Abc"), //
             Arguments.of("APPLE", "Apple"), //
             Arguments.of("gooD", "Good"));
 }

上面的代码很容易理解:静态方法testCapitalize()返回了一组测试参数,每个参数都包含两个String,正好作为测试方法的两个参数传入。

如果静态方法和测试方法的名称不同,@MethodSource也允许指定方法名。但使用默认同名方法最方便。

另一种传入测试参数的方法是使用@CsvSource,它的每一个字符串表示一行,一行包含的若干参数用,分隔

 @ParameterizedTest
 @CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" })
 void testCapitalize(String input, String result) {
     assertEquals(result, StringUtils.capitalize(input));
 }

如果有成百上千的测试输入,那么,直接写@CsvSource就很不方便。这个时候,我们可以把测试数据提到一个独立的CSV文件中,然后标注上@CsvFileSource

 @ParameterizedTest
 @CsvFileSource(resources = { "/test-capitalize.csv" })
 void testCapitalizeUsingCsvFile(String input, String result) {
     assertEquals(result, StringUtils.capitalize(input));
 }

JUnit只在classpath中查找指定的CSV文件,因此,test-capitalize.csv这个文件要放到test目录下,内容如下:

 apple, Apple
 HELLO, Hello
 JUnit, Junit
 reSource, Resource