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));
}
}
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();});
}
超时测试
assertTimeout和assertTimeoutPreemptively
将入参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);
}
}
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);
}
测试名称自定义
测试执行时,会在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);
}
重复测试
使用@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属性中可以使用currentRepetition
和totalRepetitions
占位符
嵌套测试
- 如果一个测试类中有很多测试方法(如增删改查,每种操作都有多个测试方法),那么不论是管理还是结果展现都会显得比较复杂,此时嵌套测试(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");
}
}
}