JUnit

young 1,263 2022-05-08

SpringBoot与JUnit

SpringBoot 2.4.0之后,spring-boot-starter-test默认仅支持JUnit5

SpringBoot2.4.0之前,默认使用JUnit5,同时兼容支持JUnit4,使用时需在测试类上加上@RunWith(SpringRunner.class)注解

spring-boot-starter-test版本大于2.2小于2.4时,若想只用Junit5,需要excluding org.junit.vintage:junit-vintage-engine,以刨除Junit4的干扰。

命名规范

类名:被测试类+Test,如CalculateTest

​ Test+被测试类,如TestCalculate

方法名:test+被测试方法,方法的首字母大写,如testAdd,或者直接使用方法名,如add

测试类定义为public

测试方法定义为public,且无返回值,参数列表为空

Maven执行Test

# 执行所有测试
mvn test 
# 执行指定测试类中所有test
mvn test -Dtest=CalculateTest
# 执行指定测试类中的某一方法
mvn test -Dtest=CalculateTest#testAdd

JUnit4

注解

注解 描述
@Test 测试方法
expected=XXException.class:如果程序的异常和XXException.class一样,则测试通过
timeout=100:如果程序的执行能在100ms之内完成,则测试通过
@Before 在每个测试方法之前运行
@After 在每个测试方法之后运行
@BeforeClass 方法必须是静态方法(static声明),所有测试开始之前运行,只运行一次
@AfterClass 方法必须是静态方法(static声明),所有测试结束之后运行,只运行一次
@Ignore 被忽略的测试方法,加上之后,暂时不运行此段代码
@Category 标记和过滤
@RunWith 此注解放在测试类名之前,用于指明该类所使用的的测试运行器
public class JUnit4Test {
    public JUnit4Test() {
        System.out.println("构造函数");
    }

    @BeforeClass
    public static void beforeClass(){
        System.out.println("@BeforeClass");
    }

    @Before
    public void before(){
        System.out.println("@Before");
    }

    @Test
    public void test(){
        System.out.println("@Test");
    }

    @Ignore
  	@Test
    public void ignore(){
        System.out.println("@Ignore");
    }

    @After
    public void after(){
        System.out.println("@After");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println("@AfterClass");
    }
}
@BeforeClass
构造函数
@Before
@Test
@After
@AfterClass
public class JUnit5Test {
    public JUnit5Test() {
        System.out.println("构造函数");
    }

    @BeforeAll
    public static void beforeClass(){
        System.out.println("@BeforeAll");
    }

    @BeforeEach
    public void before(){
        System.out.println("@BeforeEach");
    }

    @Test
    public void test(){
        System.out.println("@Test");
    }

    @Disabled
  	@Test
    public void ignore(){
        System.out.println("@Ignore");
    }

    @AfterEach
    public void after(){
        System.out.println("@AfterEach");
    }

    @AfterAll
    public static void afterClass(){
        System.out.println("@AfterAll");
    }
}
@BeforeAll
构造函数
@BeforeEach
@Test
@AfterEach
@AfterAll

断言 Assert

断言 描述
Assert.assertEquals(boolean expected, boolean actual) 判断两个对象是否相等
Assert.assertArrayEquals(expectedArray, resultArray) 判断两个数组是否相等
Assert.assertTrue(boolean condition) 检查条件是否为真
Assert.assertFalse(boolean condition) 检查条件是否为假
Assert.assertNull(object) 检查对象是否为空
Assert.assertNotNull(object) 检查对象是否非空
Assert.assertSame(expected, actual) 检查两个对象的引用是否相等
Assert.assertNotSame(expected, actual) 检查两个对象的引用是否不相等

被测试的类

public class Calculate {
    public int add(int a,int b){
        return a+b;
    }

    public int subtract(int a,int b){
        return a-b;
    }
}

测试类

import org.junit.Assert;
import org.junit.Test;

public class CalculateTest {

    @Test
    public void testAdd(){
        Calculate calculate = new Calculate();
        Assert.assertEquals(5,calculate.add(3,2));
    }

    @Test
    public void testSubtract(){
        Calculate calculate = new Calculate();
        Assert.assertEquals(1,calculate.subtract(3,2));
    }
}

通过@BeforeClass进行初始化

public class CalculateTest {
    static Calculate calculate;
    @BeforeClass
    public static void initCalculate(){
        calculate = new Calculate();
    }

    @Test
    public void testAdd(){
        Assert.assertEquals(5,calculate.add(3,2));
    }

    @Test
    public void testSubtract(){
        Assert.assertEquals(1,calculate.subtract(3,2));
    }
}

测试异常结果

public class CalculateTest {
    static Calculate calculate;
    @BeforeClass
    public static void initCalculate(){
        calculate = new Calculate();
    }

    @Test
    public void testAdd(){
        Assert.assertEquals(6,calculate.add(3,2));
    }

    @Test
    public void testSubtract(){
        Assert.assertEquals(-1,calculate.subtract(3,2));
    }
}
java.lang.AssertionError: 
Expected :6
Actual   :5

java.lang.AssertionError: 
Expected :-1
Actual   :1

套件测试

  • Suite Test:JUnit4可以将多个测试类组合在一起执行测试,称为套件测试
  • 通过@RunWith注解指明测试的运行器
  • 通过@SuiteClasses注解指定多个测试类

测试类A

public class CalculateATest {
    static Calculate calculate;
    @BeforeClass
    public static void initCalculate(){
        calculate = new Calculate();
    }


    @Test
    public void testSubtract(){
        Assert.assertEquals(0,calculate.subtract(3,2));
    }
}

测试类B

public class CalculateBTest {
    static Calculate calculate;
    @BeforeClass
    public static void initCalculate(){
        calculate = new Calculate();
    }

    @Test
    public void testAdd(){
        Assert.assertEquals(8,calculate.add(3,2));
    }

}

套件测试类

@RunWith(Suite.class)
@Suite.SuiteClasses({CalculateATest.class,CalculateBTest.class})
public class SuiteTest {
}

执行套件测试类,将会执行SuiteClasses中标注测试类中的所有测试用例

参数化测试

  • Parameterized Test,使用不同的参数值来进行测试的测试

  • 通过@RunWith(Parameterized.class)注解来说明对某个测试类进行参数话测试

  • 测试类中必须有一个public static的方法,并且用@Parameters注解进行标注,返回类型为一个对象的集合

被测试对象

public class Fibonacci {

    public static int calculate(int n){
        if (n<=2){
            return 1;
        }
        int a1 =1;
        int a2=1;
        int a3 = 0;
        for (int i = 3; i <=n;i++) {
            a3 = a2 + a1;
            a1 = a2;
            a2 = a3;
        }
        return a3;
    }
}

测试类

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.Arrays;
import java.util.Collection;

@RunWith(Parameterized.class)
public class FibonacciTest {
    private int num;
    private int result;


    public FibonacciTest(int num, int result) {
        this.num = num;
        this.result = result;
    }

    @Parameterized.Parameters
    public static Collection<Object[]> param(){
        return Arrays.asList(new Object[][]{
                {1,1},
                {2,1},
                {3,2},
                {4,3},
                {5,5},
                {6,8}
        });
    }

    @Test
    public void testCalculate(){
        Assert.assertEquals(result,Fibonacci.calculate(num));
    }

}

JUnit5

参考:https://blog.csdn.net/boling_cavalry/article/details/108810587

官方文档:https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

注解

在JUnit5中,注解发生了变化

JUnit4 JUnit5 描述
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Before @BeforeEach
@After @AfterEach
@Ignore @Disabled
@Category @Tag
N/A @TestFactory 测试工厂进行动态测试
N/A @Nested 嵌套测试
N/A @ExtendWith 注册自定义扩展
N/A @DisplayName 测试方法的展示名称
N/A @Timeout 超时设置
@RunWith @ExtendWith
@Rule @ExtendWith
@ClassRule @RegistryExtension

Assumptions(假设)与Assertions(断言)

Assertions提供的方法,如果条件不满足,会抛出AssertionFailedError异常,JUnit对抛出此异常的方法判定为失败

Assumptions提供的方法,如果条件不满足,会抛出TestAbortedException,JUnit对抛出此异常的方法判定为跳过。

Assumptions

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@Slf4j
public class AssertAssumeTest {
    /**
     * 最简单的成功用例
     */
    @Test
    void assertSuccess() {
        assertEquals(2, Math.addExact(1,1));
    }

    /**
     * 最简单的失败用例
     */
    @Test
    void assertFail() {
        assertEquals(3, Math.addExact(1,1));
    }

    /**
     * assumeTrue不抛出异常的用例
     */
    @Test
    void assumeSuccess() {
        // assumeTrue方法的入参如果为true,就不会抛出异常,后面的代码才会继续执行
        assumeTrue(true);
        // 如果打印出此日志,证明assumeTrue方法没有抛出异常
        log.info("assumeSuccess的assumeTrue执行完成");
        // 接下来是常规的单元测试逻辑
        assertEquals(2, Math.addExact(1,1));
    }

    /**
     * assumeTrue抛出异常的用例
     */
    @Test
    void assumeFail() {
        // assumeTrue方法的入参如果为false,就会抛出TestAbortedException异常,后面就不会执行了
        assumeTrue(false, "未通过assumeTrue");
        // 如果打印出此日志,证明assumeFail方法没有抛出异常
        log.info("assumeFail的assumeTrue执行完成");
        // 接下来是常规的单元测试逻辑,但因为前面抛出了异常,就不再执行了
        assertEquals(2, Math.addExact(1,1));
    }

}

JunitAssume.png

Assertions

assertArrayEquals

private static void assertArrayEquals(short[] expected, short[] actual, Deque<Integer> indexes,
      Object messageOrSupplier) {

   if (expected == actual) { // 是否同一引用地址
      return;
   }
   assertArraysNotNull(expected, actual, indexes, messageOrSupplier); // 是否为空
   assertArraysHaveSameLength(expected.length, actual.length, indexes, messageOrSupplier);  // 数组长度是否相同

   for (int i = 0; i < expected.length; i++) {
     	// 逐个元素进行比较
      if (expected[i] != actual[i]) {
         failArraysNotEqual(expected[i], actual[i], nullSafeIndexes(indexes, i), messageOrSupplier);
      }
   }
}

assertAll

可以将多个判断逻辑放在一起处理,只要有一个报错就会导致整体测试不通过,并且执行结果中会给出具体的失败详情

@Test
@DisplayName("批量判断")
void testAssetAll(){
    assertAll("批量测试",
            ()->assertEquals(1,1),
            ()->assertEquals(2,1),
            ()->assertEquals(3,1));
}
Comparison Failure: 
Expected :2
Actual   :1
<Click to see difference>



expected: <3> but was: <1>
Comparison Failure: 
Expected :3
Actual   :1
<Click to see difference>

assertThrows

测试方法执行时是否会抛出指定异常,如果不抛出异常或者异常与期望不一致,则测试失败

@Test
    @DisplayName("测试抛出指定异常")
    void testAssertThrows(){
        assertThrows(NullPointerException.class,()->{String a = null;a.getBytes();});
        assertThrows(IndexOutOfBoundsException.class,()->{String a = null;a.getBytes();});
    }

超时测试

assertTimeoutassertTimeoutPreemptively

将入参Executable的execute方法执行完成后,再检查execute方法的耗时是否超过预期,这种方法的弊端是必须等待execute方法执行完成才知道是否超时,assertTimeoutPreemptively方法也是用来检测代码执行是否超时的,但是避免了assertTimeout的必须等待execute执行完成的弊端,避免的方法是用一个新的线程来执行execute方法

第三方断言库

除了junit的Assertions类,还可以选择第三方库提供的断言能力,比较典型的有AssertJ, Hamcrest, Truth这三种,springboot默认依赖了hamcrest库

按条件执行

自定义测试方法的执行顺序

  • 给测试类添加@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
  • 给测试方法添加@Order注解,value值为数字,value越小越优先执行
@Test
void testOrderFirst(){
    System.out.println("order 2");
}

@Test
void testOrderLast(){
    System.out.println("order 1");
}
order 2
order 1

加上order相关注解

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FunctionOrderTest {
    
    @Order(2)
    @Test
    void testOrderFirst(){
        System.out.println("order 2");
    }
    
    @Order(1)
    @Test
    void testOrderLast(){
        System.out.println("order 1");
    }
}
order 1
order 2

按操作系统设置条件

  • @EnabledOnOs指定多个系统,满足其中一个条件,测试方法会执行
  • @DisabledOnOs指定多个系统,满足其中一个条件,测试方法不执行
@Order(2)
@DisabledOnOs({OS.MAC,OS.WINDOWS})
@Test
void testOrderFirst(){
    System.out.println("order 2");
}

@Order(1)
@EnabledOnOs({OS.MAC})
@Test
void testOrderLast(){
    System.out.println("order 1");
}
order 1

Disabled on operating system: Mac OS X

按Java环境设置条件

  • @EnabledOnJre指定多个JRE版本,当前JRE是其中一个,测试方法执行
  • @DisabledOnJre指定多个JRE版本,当前JRE是其中一个,测试方法不执行
  • @EnabledForJreRange指定JRE范围,当前JRE在范围内,测试方法执行
  • @DisabledForJreRange指定JRE范围,当前JRE在范围内,测试方法不执行
@Order(2)
@DisabledOnJre({JRE.JAVA_8,JRE.JAVA_17})
@Test
void testOrderFirst(){
    System.out.println("order 2");
}

@Order(1)
@EnabledForJreRange(min = JRE.JAVA_8,max = JRE.JAVA_17)
@Test
void testOrderLast(){
    System.out.println("order 1");
}
order 1

Disabled on JRE version: 1.8.0_322

按系统属性设置条件

  • @EnabledIfSystemProperty
  • @DisabledIfSystemProperty
  • 指定系统属性的key和期望值(模糊匹配),只要当前系统有此属性并且值也匹配
@Order(2)
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
@Test
void testOrderFirst(){
    System.out.println("order 2");
}

@Order(1)
@EnabledIfSystemProperty(named = "java.vm.name", matches = ".*window.*")
@Test
void testOrderLast(){
    System.out.println("order 1");
}
System property [java.vm.name] with value [OpenJDK 64-Bit Server VM] does not match regular expression [.*window.*]
order 2

按环境变量设置条件

  • @EnabledIfEnvironmentVariable
  • @DisabledIfEnvironmentVariable
  • 指定环境变量的key和期望值(模糊匹配),只要当前系统有此环境变量并且值也匹配
@Order(2)
@EnabledIfEnvironmentVariable(named = "JAVA_HOME", matches = "..*")
@Test
void testOrderFirst(){
    System.out.println("order 2");
}

@Order(1)
@DisabledIfEnvironmentVariable(named = "GOPATH", matches = ".*")
@Test
void testOrderLast(){
    System.out.println("order 1");
}
Environment variable [GOPATH] with value [/Users/young/Desktop/go_learning] matches regular expression [.*]
order 2

自定义条件

junit5.7版本开始

  • @EnabledIf
  • @DisabledIf

参数为当前类的方法名,该方法需返回boolean类型

如果用在类上,方法必须为static方法

如果使用非同类的方法,规则为完整类路径#方法名,如com.example.Conditions#customCondition

参数化测试

  • @ParameterizedTest代替@Test
  • 使用@ValueSource指定每次测试时参数,每个参数执行一次
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FunctionOrderTest {
    
    @Order(2)
    @ParameterizedTest
    @ValueSource(strings = {"1","2","3"})
    public void testOrderFirst(String value){
        System.out.println("value="+value);
        assertTrue(value!=null);
    }
}

JunitParemeterizedTest1.png

ValueSource

ValueSource是最简单常用的数据源

支持short,byte,int,long,float,double,char,boolean,java.lang.String,java.lang.Class

使用@NullValue表示注入null元素,使用@EmptySource代表空字符串入川

也可以使用@NullAndEmptySource可以同时使用null和空字符串做测试方法的入参

枚举数据源

@EnumSource可以让一个枚举类中的全部或者部分值作为测试方法的入参

定义枚举

public enum TestEnum {
    ONE,
    TWO,
    THREE
}
@ParameterizedTest
@EnumSource
void enumSourceTest(TestEnum value) {
    System.out.println("value=" + value);
    assertTrue(value != null);
}
value=ONE
value=TWO
value=THREE

如果只要枚举中的一部分,可以在name属性中进行指定

@EnumSource(names=("ONE","THREE"))

也可以指定哪些值不被执行,需要添加mode属性,并设置为EXCLUDE,如果不写,默认是INCLUDE

@EnumSource(mode=EnumSource.EXCLUDE,names={"ONE","THREE"})

方法数据源

  • 使用@MethodSource指定一个方法名称,该方法返回的元素集合作为测试方法的入参
  • 定义的方法一般是static类型,否则要用@TestInstance修饰,并且返回值是Stream类型
static Stream<String> stringProvider(){
    return Stream.of("test1","test2");
}

@ParameterizedTest
@MethodSource({"stringProvider"})
void methodResourceTest(String value){
    System.out.println(value);
}

如果参数方法和测试方法不在同一个类中,要使用全限定类名#方法名的形式

如果不在@MethodResource中指定方法名,JUnit会寻找和测试方法名相同的静态方法

static Stream<String> noMethodSourceNameTest(){
    return Stream.of("testA","testB");
}

@ParameterizedTest
@MethodSource
void noMethodSourceNameTest(String value){
    System.out.println(value);
}

CSV格式数据

@ParameterizedTest
@CsvSource({
        "young,12",
        "faker,16"
})
void testCsvSource(String name,int age){
    System.out.printf("name:%s,age:%d\n",name,age);
}

@CsvSource提供了一个属性nullValues,作用是将指定的字符串识别为null

@ParameterizedTest
@CsvSource(value = {
        "young,12",
        "faker,16",
        "NA,30",
},nullValues = "NA")
void testCsvSource(String name,int age){
    System.out.printf("name:%s,age:%d\n",name,age);
}

Csv文件数据源

@CsvFileSource可以指定CSV文件作为数据源,numLinesToSkip属性可以指定跳过的行数

创建文件TestCsvSource.csv

name,age
abc,123
ff,456
uuu,789
NA,0
@ParameterizedTest
@CsvFileSource(files = {"src/test/resources/TestCsvSource.csv"},nullValues = "NA",numLinesToSkip = 1)
void testCsvFileSource(String name,int age){
    System.out.printf("name:%s,age:%d\n",name,age);
}

自定义数据源

  • 实现ArgumentsProvider接口
  • 使用@ArgumentsSource指定
public class MyArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
        return Stream.of("args1","args2").map(Arguments::of);
    }
}
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testMyArgumentsProvider(String value){
    System.out.println(value);
}

参数转换

JUnit5对参数类型和测试方法入参没有严格要求的一致性,JUnit5可以做一写自动或手动类型转换

@ParameterizedTest
@ValueSource(ints = {1,2,3})
void testAutoConvert(double value){
    System.out.println(value);
}
1.0
2.0
3.0
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testTimeConvert(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate value){
    System.out.println(value);
}

字段聚合

@ParameterizedTest
@CsvSource(value = {
        "young,12",
        "faker,16",
        "NA,30",
}, nullValues = "NA")
void testCsvSource(String name, int age) {
    System.out.printf("name:%s,age:%d\n", name, age);
}

这种方式,对于参数较少的数据还可以,但是如果字段较多,就显然不太合适了。

JUnit5提供了字段聚合功能Argument Aggregation,可以将CSV每条记录的所有字段都放入到ArgumentsAccessor类型的对象中,测试方法只需声明ArgumentsAccessor类型作为入参

@ParameterizedTest
@CsvSource(value = {
        "young,12,ONE",
        "faker,16,TWO",
        "NA,30,THREE",
}, nullValues = "NA")
void testArgumentsAccessor(ArgumentsAccessor arguments) {
    String name = arguments.getString(0);
    Integer age = arguments.getInteger(1);
    TestEnum testEnum = arguments.get(2, TestEnum.class);
    Person person = new Person(name, age, testEnum);
    System.out.println(person);
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person{
    private String name;
    private int age;
    private TestEnum testEnum;
}

自定义聚合

JUnit5在测试方法入参上初提供了一个注解@AggregateWith,需要传入一个ArgumentsAggregator,就可以对数据进行聚合转换

自定义一个Person对象的聚合转换器

public class PersonAggregator implements ArgumentsAggregator {

    @Override
    public Object aggregateArguments(ArgumentsAccessor argumentsAccessor, ParameterContext parameterContext) throws ArgumentsAggregationException {
        Person person = new Person();
        person.setName(argumentsAccessor.getString(0));
        person.setAge(argumentsAccessor.getInteger(1));
        person.setTestEnum(argumentsAccessor.get(2, TestEnum.class));
        return person;
    }
}

使用@AggregateWith声明聚合转换器

@ParameterizedTest
@CsvSource(value = {
        "young,12,ONE",
        "faker,16,TWO",
        "NA,30,THREE",
}, nullValues = "NA")
void testAggregator(@AggregateWith(PersonAggregator.class)Person person){
    System.out.println(person);
}

自定义注解

创建自定义注解,将@AggregateWith标注在自定义注解上

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}

使用自定义注解

@ParameterizedTest
@CsvSource(value = {
        "young,12,ONE",
        "faker,16,TWO",
        "NA,30,THREE",
}, nullValues = "NA")
void testAggregatorAnnotation(@CsvToPerson Person person){
    System.out.println(person);
}

测试名称自定义

JunitParameterizedParam.png

测试执行时,会在IDEA中显示序号和参数,可以通过@ParameterizedTest的name属性进行定制

@ParameterizedTest(name = "序号:{index},name:{0},age:{1},TestEnum:{2}")
@CsvSource(value = {
        "young,12,ONE",
        "faker,16,TWO",
        "NA,30,THREE",
}, nullValues = "NA")
void testAggregatorAnnotation(@CsvToPerson Person person){
    System.out.println(person);
}

JunitParameterizedParam1.png

重复测试

使用@RepeatedTes代替@Test

@RepeatedTest(6)
void testRepeated(TestInfo testInfo){
		System.out.println(testInfo.getTestMethod().get().getName());
}

RepetitionInfo可以获得重复的信息

@RepeatedTest(6)
void testRepeated(TestInfo testInfo, RepetitionInfo repetitionInfo) {
    System.out.println(testInfo.getTestMethod().get().getName() + ":" 
            + repetitionInfo.getCurrentRepetition() + "/" + repetitionInfo.getTotalRepetitions());
}

@RepeatedTest的name属性中可以使用currentRepetitiontotalRepetitions占位符

嵌套测试

  • 如果一个测试类中有很多测试方法(如增删改查,每种操作都有多个测试方法),那么不论是管理还是结果展现都会显得比较复杂,此时嵌套测试(Nested Tests)就派上用场了
  • 嵌套测试(Nested Tests)功能就是在测试类中创建一些内部类,以增删改查为例,将所有测试查找的方法放入一个内部类,将所有测试删除的方法放入另一个内部类,再给每个内部类增加@Nested注解,这样就会以内部类为单位执行测试和展现结果
public class NestedTest {
    
    @Nested
    class NestedClass{
        @Test
        void test1(){
            System.out.println("test1");
        }
        @Test
        void test2(){
            System.out.println("test2");
        }
    }

    @Nested
    class Nested2Class{
        @Test
        void test3(){
            System.out.println("test3");
        }
        @Test
        void test4(){
            System.out.println("test4");
        }
    }
}

NestedTest.png