🤖🤖-摘要:
本文介绍了SpringBoot单元测试的方法和使用。通过JUnit和MockMvc框架,可以精确控制测试粒度,提高单元测试的效率和质量。文章详述了Springboot单元测试相关注解,断言,嵌套测试以及参数化测试的使用方法,帮助开发者系统性的理解和掌握SpringBoot单元测试相关知识。

概述

在平时的开发当中,一个项目往往包含了大量的方法,可能有成千上万个。如何去保证这些方法产生的结果是我们想要的呢?

传统解决方案:Postman 发报文,System.out打印debug日志,或者眼睛看返回报文

  1. 眼睛看结果是否正确,瞅瞎不说,也太不智能.我们是高智商程序员,能让代码解决的事情,绝不能靠人工去解决
  2. postman 只能对controller进行测试。controller要正确,前提是service,dao都正确。发现问题太晚,解决成本高
  3. 对于一些交易系统,由于交易主键的存在,每次都要更改参数后,再进行测试,效率太低
  4. 无法对内部的函数功能做测试
  5. postman的测试案例与项目工程不再一起,这些案例只能自己一个人用,无法团队共享

这时,就轮到单元测试闪亮出场了

  • 测试代码和工程代码在同一工程文件中,便于维护和传承
  • 使用断言自动检测结果
  • 测试粒度小,可以小到每个函数
  • 测试模块间相互依赖小。开发完一个模块,就可以测试一个模块。妈妈再也不用担心我犯大错了

SpringBoot单元测试

业界单元测试一般采用基于JUnitMockMvc框架进行

  • JUnit: 是通用测试框架,主要进行Dao层和Service层测试
  • MockMvc: 主要进行Controller层测试

相关注解

JUnit5的注解与JUnit4的注解有所变化,详见官方文档

  • @Test:表示方法是测试方法。但是与JUnit4@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest: 表示方法是参数化测试
  • @RepeatedTest: 表示方法可重复执行
  • @DisplayName: 为测试类或者测试方法设置展示名称
  • @BeforeEach: 表示在每个单元测试之前执行
  • @AfterEach: 表示在每个单元测试之后执行
  • @BeforeAll: 表示在所有单元测试之前执行
  • @AfterAll: 表示在所有单元测试之后执行
  • @Tag: 表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled: 表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • @Timeout: 表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith: 为测试类或测试方法提供扩展类引用
class StandardTests {

@BeforeAll //表示在所有单元测试之前执行
static void initAll() {
System.out.println("开始单元测试:");
}

@BeforeEach //表示在每个单元测试之前执行
void init() {
System.out.println("方法执行前...");
}

@DisplayName("测试名称")
@Test
void succeedingTest() {
}

@Test
void failingTest() {
fail("a failing test");
}

@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}

@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}

@AfterEach
void tearDown() {
System.out.println("方法执行结束...");
}

@AfterAll
static void tearDownAll() {
System.out.println("单元测试结束");
}

}

断言

方法说明
assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertSame判断两个对象引用是否指向同一个对象
assertNotSame判断两个对象引用是否指向不同的对象
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null
assertArrayEquals数组断言
assertAll组合断言
assertThrows异常断言
assertTimeout超时断言
fail快速失败

嵌套测试

JUnit5可以通过Java中的内部类和@Nested注解实现嵌套测试, 从而可以更好的把相关的测试方法组织在一起.
在内部类中可以使用@BeforeEach@AfterEach注解, 而且嵌套的层次没有限制

@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> method() {
return Stream.of("apple", "banana");
}