Skip to content

Latest commit

 

History

History
274 lines (188 loc) · 7.87 KB

junit.md

File metadata and controls

274 lines (188 loc) · 7.87 KB

JUnit

单元测试就是针对最小的功能单元编写测试代码,Java程序最小的功能单元是方法,因此对Java程序进行单元测试就是针对单个Java方法的测试。

JUnit是一个单元测试框架,专门用于运行我们编写的单元测试:

一个JUnit测试包含若干@Test方法,并使用Assertions进行断言,注意浮点数assertEquals()要指定delta

assertEquals(0.1, Math.abs(1 - 9 / 10.0), 0.0000001);

优势

使用JUnit编写单元测试的好处在于,我们可以非常简单地组织测试代码,并随时运行它们,JUnit就会给出成功的测试和失败的测试,还可以生成测试报告,不仅包含测试的成功率,还可以统计测试的代码覆盖率,即被测试的代码本身有多少经过了测试。对于高质量的代码来说,测试覆盖率应该在80%以上。

Fixture

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

编写Fixture是指针对每个@Test方法,编写@BeforeEach方法用于初始化测试资源,编写@AfterEach用于清理测试资源;必要时,可以编写@BeforeAll@AfterAll,使用静态变量来初始化耗时的资源,并且在所有@Test方法的运行前后仅执行一次。

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));
    }
}

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

public class DatabaseTest {
    static Database db;

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

因此,我们总结出编写Fixture的套路如下:

  1. 对于实例变量,在@BeforeEach中初始化,在@AfterEach中清理,它们在各个@Test方法中互不影响,因为是不同的实例;
  2. 对于静态变量,在@BeforeAll中初始化,在@AfterAll中清理,它们在各个@Test方法中均是唯一实例,会影响各个@Test方法。

参数化测试

使用参数化测试,可以提供一组测试数据,对一个测试方法反复测试。JUnit提供了一个@ParameterizedTest注解,用来进行参数化测试。

参数既可以在测试代码中写死,也可以通过@CsvFileSource放到外部的CSV文件中。

@ParameterizedTest
@ValueSource(ints = { 0, 1, 5, 100 })
void testAbs(int x) {
    assertEquals(x, Math.abs(x));
}

实际的测试场景往往没有这么简单。假设我们自己编写了一个StringUtils.capitalize()方法,它会把字符串的第一个字母变为大写,后续字母变为小写:

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
void testCapitalize(String input, String result) {
    assertEquals(result, StringUtils.capitalize(input));
}

现在问题来了:参数如何传入?

最简单的方法是通过@MethodSource注解,它允许我们编写一个同名的静态方法来提供测试参数:

@ParameterizedTest
@MethodSource
void testCapitalize(String input, String result) {
    assertEquals(result, StringUtils.capitalize(input));
}

static List<Arguments> testCapitalize() {
    return List.of( // arguments:
            Arguments.arguments("abc", "Abc"), //
            Arguments.arguments("APPLE", "Apple"), //
            Arguments.arguments("gooD", "Good"));
}

如果静态方法和测试方法的名称不同,@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自动忽略某些测试。

在运行测试的时候,有些时候,我们需要排出某些@Test方法,不要让它运行,这时,我们就可以给它标记一个@Disabled

@Disabled
@Test
void testBug101() {
    // 这个测试不会运行
}

但如果注释掉@Test,JUnit就不知道这是个测试方法,而加上@Disabled,JUnit仍然识别出这是个测试方法,只是暂时不运行。它会在测试结果中显示:

Tests run: 68, Failures: 2, Errors: 0, Skipped: 5

我们来看一些常用的条件测试:

不在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
}

最后,万能的@EnableIf可以执行任意Java语句并根据返回的boolean决定是否执行测试。下面的代码演示了一个只能在星期日执行的测试:

@Test
@EnabledIf("java.time.LocalDate.now().getDayOfWeek()==java.time.DayOfWeek.SUNDAY")
void testOnlyOnSunday() {
    // TODO: this test is only run on Sunday
}

异常测试

测试异常可以使用assertThrows(),期待捕获到指定类型的异常;对可能发生的每种类型的异常都必须进行测试。

assertThrows()在捕获到指定异常时表示通过测试,未捕获到异常,或者捕获到的异常类型不对,均表示测试失败。

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

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

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

参考

那些年,我们写过的无效单元测试