Spring Boot + JUnit 5 编写单元测试,看着一篇就够了-Spring专区论坛-技术-SpringForAll社区

Spring Boot + JUnit 5 编写单元测试,看着一篇就够了

一、前言

在我们日常开发中项目功能自测必不可少,但是又是很容易被忽视的一个环节,其实,个人认为测试才是一个项目良好运行的保障,好的测试习惯能帮我们避免很多问题,可以提高我们的思考和开发功能的效率,但是一个复杂的项目的测试往往是让人头痛不已,千丝万缕连在一起,除非还原用户使用场景,否则怎么测试都让程序员感到不安,往往写完代码提交之后寄希望于测试爸爸能测出漏洞,但是自己写的代码,薄弱点和漏洞出现的可能性也只有自己亲自知道该往那个方向测试重拳出击,测试又怎么会知道呢?往往项目上线之后我们还如履薄冰,要怎么测试自己开发的功能一直以来都是每个开发工作者需要考虑的问题,但是测试又分很多种类,比如一个小小的逻辑模块不依赖于任何模块的测试,像我来说就一个main方向就搞定了,如果在依赖深一点的,需要配置文件,需要web容器,需要bean的嵌套,但是又没有完整的环境,或者说配置一套完整的环境实在是太麻烦的时候,我们应该怎么测试呢?对了,如果你刚好是SpringBoot项目,那么你一定要了解一下SpringBootTest,在SpringBoot中集成Junit5来进行测试,这也是目前市场主流的测试框架。

二、快速使用Spring Boot Test

1、JUnit 集成 Spring Boot

Spring Boot 2.2.x 之前使用的是 Junit4,之后就使用的是Junit5。

maven引入依赖,仅需要引入starter依赖就足够了。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.3.13.RELEASE</version>
    <scope>test</scope>
</dependency>

2、一个简单的测试类

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DemoTest {

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

三、SpringTest 注解

Spring为了简化xml文件配置,使用了大量的注解,所以需要我们重点了解SpringBootTest的注解,以便了解SpringBootTest的运行机制。

@SpringBootTest 它会加载完整的Spring应用程序上下文,包括所有的bean定义、配置和组件,并且会自动启动嵌入式的服务器。 @SpringBootTest 参数配置

  • 1、value 指定”my.application.property”的值为”value”
@SpringBootTest(value = "my.application.property=value")
  • 2、properties 在加载配置的时候修改my.property my.otherProperty
@SpringBootTest(properties = {"my.property=value1", "my.otherProperty=value2"})
  • 3、classes 指定两个配置类MyConfig1.class和MyConfig2.class
@SpringBootTest(classes = {MyConfig1.class, MyConfig2.class})
  • 4、webEnvironment 指定使用随机端口的Web环境进行集成测试。
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
可选值 说明
MOCK 此值为默认值,该类型提供一个mock环境,此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web端口。
RANDOM_PORT 启动一个真实的web服务,监听一个随机端口。
DEFINED_PORT 启动一个真实的web服务,监听一个定义好的端口(从配置中读取)。
NONE 启动一个非web的ApplicationContext,既不提供mock环境,也不提供真是的web服务

@Test 用来标记这个方法是个测试方法。

@SpringBootTest
public class DemoTest {
    /**
     * 表示这个方法是个测试方法
     */
    @Test
    public void demoTest()
    {
        System.out.println("test success");
    }
}

@ParameterizedTest 用来标记一个参数化测试方法,和以下注解搭配使用。(列出常用的几种入参方式注解)

  • @ValueSource:用于指定基本类型、String类型或Class类型的参数值。
  • @CsvSource:用于指定CSV格式的参数值,逗号分隔每个参数。
  • @MethodSource:用于指定一个或多个方法作为参数提供者。
  • @ArgumentsSource:用于指定一个自定义的参数提供者类。
  • @NullSource:用于指定null值作为参数。
  • @EmptySource:用于指定空值作为参数。
  • @NullAndEmptySource:用于指定null值和空值作为参数。
  • @DisplayNameGeneration:用于指定一个自定义的显示名称生成器。

这些注解提供了不同的方式来为测试方法提供参数,并且可以根据实际情况选择合适的注解来进行参数化测试。

public class DemoTest {
    /**
     * @ParameterizedTest标记名为testIsPositive的参数化测试方法
     * @ValueSource注解,我们将整数1、2和3作为参数传递给测试方法
     * @param number
     */
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    public void testIsPositive(int number) {
        assertTrue(number > 0);
    }
}
  • 1、@ValueSource 用于提供测试方法的参数化值。
public class CalculatorTest {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    public void testIsPositive(int number) {
        assertTrue(number > 0);
    }
}
  • 2、@MethodSource 用于提供测试方法的参数化方法。
public class CalculatorTest {

    @ParameterizedTest
    @MethodSource("provideAdditionTestData")
    public void testAddition(int a, int b, int expected) {
        Calculator calculator = new Calculator();
        int result = calculator.add(a, b);
        assertEquals(expected, result);
    }

    private static Stream<Arguments> provideAdditionTestData() {
        return Stream.of(
                Arguments.of(1, 2, 3),
                Arguments.of(5, -3, 2),
                Arguments.of(10, 10, 20)
        );
    }
}
  • 3、@NullSource 用于指定测试方法的参数化测试中的一个参数为null。
public class StringUtilsTest {

    @ParameterizedTest
    @NullSource
    public void testIsEmpty_Null(String input) {
        assertTrue(StringUtils.isEmpty(input));
    }
}
  • 4、@EnumSource 用于指定测试方法的参数化测试中的一个参数为枚举类型的值。

public class CalculatorTest {

    enum Operation {
        ADD, SUBTRACT, MULTIPLY, DIVIDE
    }

    @ParameterizedTest
    @EnumSource(Operation.class)
    public void testCalculate(Operation operation) {
        Calculator calculator = new Calculator();

        int result;
        switch (operation) {
            case ADD:
                result = calculator.add(2, 3);
                assertEquals(5, result);
                break;
            case SUBTRACT:
                result = calculator.subtract(5, 2);
                assertEquals(3, result);
                break;
            case MULTIPLY:
                result = calculator.multiply(4, 6);
                assertEquals(24, result);
                break;
            case DIVIDE:
                result = calculator.divide(10, 2);
                assertEquals(5, result);
                break;
        }
    }
}

@RepeatedTest 用于重复运行相同的测试方法。可以使用该注解指定要重复运行的次数。

public class DemoTest {
    /**
     * @RepeatedTest(5) 标记名为testAddition的测试方法,并指定要重复运行它的次数为5次
     */
    @RepeatedTest(5)
    public void testAddition() {
        int result = 2 + 2;
        assertEquals(4, result);
    }
}

@DisplayName 用于为测试类或测试方法指定一个自定义的显示名称。

@DisplayName("Calculator Tests")
public class CalculatorTest {

    @Test
    @DisplayName("Addition Test")
    public void testAddition() {
        int result = Calculator.add(2, 2);
        assertEquals(4, result);
    }

    @Test
    @DisplayName("Subtraction Test")
    public void testSubtraction() {
        int result = Calculator.subtract(5, 3);
        assertEquals(2, result);
    }
}

@BeforeEach & @AfterEach & @BeforeAll & @AfterAll

  • @BeforeEach 在每个测试方法运行之前执行一次。
  • @AfterEach 在每个测试方法运行之后执行一次。
  • @BeforeAll 在所有测试方法之前执行一次。
  • @AfterAll 在所有测试方法之前执行一次。
public class DemoTest {

    @BeforeAll
    public static void init() {
        System.out.println("@BeforeAll");
    }
    
    @BeforeEach
    public void setUp() {
        System.out.println("@BeforeEach");
    }

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


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

    @AfterAll
    public static void cleanUp() {
        System.out.println("@AfterAll");
    }
    
}

结果

d2b5ca33bd20231108135931

 

@Disabled 用于禁用测试类或测试方法。

@Disabled("This test is currently disabled")
public class CalculatorTest {
    @Test
    public void testAddition() {
        
    }
}

@Timeout 它用于设置测试方法的超时时间。

public class CalculatorTest {
    @Test
    @Timeout(value = 5, unit = TimeUnit.SECONDS)
    public void testAddition() {
        // 执行需要在5秒内完成的测试代码
    }
}

四、Junit5断言

JUnit Jupiter 断言都是 static org.junit.jupiter.Assertions方法,方便使用。 assertEquals(expected, actual) 验证两个值是否相等。

public class DemoTest {
    @Test
    public void test() {
        int expected = 10;
        int actual = 5 + 5;
        assertEquals(expected, actual); // 通过
    }
}

assertNotEquals(unexpected, actual) 验证两个值是否不相等。

public class DemoTest {
    @Test
    public void test() {
        int unexpected = 10;
        int actual = 5 + 5;
        assertNotEquals(unexpected, actual); // 通过
    }
}

assertArrayEquals(expectedArray, actualArray) 验证两个数组是否相等。

public class DemoTest {
    @Test
    public void test() {
        int[] expectedArray = {1, 2, 3};
        int[] actualArray = {1, 2, 3};
        assertArrayEquals(expectedArray, actualArray); // 通过
    }
}

assertTrue(condition) 验证条件是否为true。

public class DemoTest {
    @Test
    public void test() {
        boolean condition = 5 > 3;
        assertTrue(condition); // 通过
    }
}

assertFalse(condition) 验证条件是否为false。

public class DemoTest {
    @Test
    public void test() {
        boolean condition = 5 < 3;
        assertFalse(condition); // 通过
    }
}

assertNull(actual) 验证值是否为null。

public class DemoTest {
    @Test
    public void test() {
        String actual = null;
        assertNull(actual); // 通过
    }
}

assertNotNull(actual) 验证值是否不为null。

public class DemoTest {
    @Test
    public void test() {
        String actual = "Hello";
        assertNotNull(actual); // 通过
    }
}

assertSame(expected, actual) 验证两个引用是否指向同一个对象。

public class DemoTest {
    @Test
    public void test() {
        String expected = "Hello";
        String actual = "Hello";
        assertSame(expected, actual); // 通过
    }
}

assertNotSame(unexpected, actual) 验证两个引用是否指向不同的对象。

public class DemoTest {
    @Test
    public void test() {
        String unexpected = "Hello";
        String actual = "World";
        assertNotSame(unexpected, actual); // 通过
    }
}

assertThrows(expectedType, executable) 验证代码块是否抛出了指定的异常。

public class DemoTest {
    @Test
    public void test() {
        assertThrows(ArithmeticException.class, () -> {
            int result = 1 / 0; // 抛出ArithmeticException
        });
    }
}

assertDoesNotThrow(executable) 验证代码块是否没有抛出任何异常。

public class DemoTest {
    @Test
    public void test() {
        assertDoesNotThrow(() -> {
            int result = 1 / 1; // 不会抛出异常
        });
    }
}

assertTimeout(duration, executable) 验证代码块是否在指定的时间内执行完毕。

public class DemoTest {
    @Test
    public void test() {
        assertTimeout(Duration.ofSeconds(5), () -> {
            // 执行耗时较长的代码块
        });
    }
}

assertAll(executables) 验证多个断言是否都通过。

public class DemoTest {
    @Test
    public void test() {
         assertAll("numbers",
            () -> assertEquals(1, 1),
            () -> assertEquals(2, 2),
            () -> assertEquals(3, 3)
        );
    }
}

assertTimeout(duration, executable) 用于在指定的时间内执行一个代码块,并在超时时抛出异常。

public class ExampleTest {
    
    @Test
    public void testMethod() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            // 执行耗时较长的代码块,不超过2秒
            Thread.sleep(1000); // 模拟耗时操作
        });
    }
}

fail() 用于在测试中直接失败并抛出一个AssertionError异常。

public class ExampleTest {
    
    @Test
    public void testMethod() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            // 执行耗时较长的代码块,不超过2秒
            Thread.sleep(1000); // 模拟耗时操作
        });
    }
}

五、MockMvc

用于对Controller进行单元测试,MockMvc使用了Spring的TestContext框架来创建一个测试上下文,其中包括模拟的Servlet容器。它会加载应用程序的配置,并创建一个模拟的DispatcherServlet实例来处理请求。
MockMvc提供了一种模拟请求和验证响应的方式,可以在不启动Web服务器的情况下对Controller进行测试。它可以模拟HTTP请求,包括GET、POST、PUT、DELETE等请求方法,并验证Controller返回的响应结果。

@SpringBootTest
@AutoConfigureMockMvc
public class DemoTest {
    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setup() {
        // 在每个测试方法执行前进行一些初始化操作
    }

    @Test
    public void testGetMethod() throws Exception {

        /**
         * get请求
         */
        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/example") // 发起GET请求,访问路径为"/example"
                .accept(MediaType.APPLICATION_JSON)// 接受JSON格式的响应
                .param("id", "123") //参数
                .param("first_flag", String.valueOf(true)); //参数


        MvcResult result = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())  // 验证请求的HTTP状态码为200
                .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("success"))  // 验证响应中的JSON字段"message"的值为"success"
                .andReturn();// 返回MockMvcResult对象

        MockHttpServletResponse response = result.getResponse(); //得到返回值
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); //设置响应 Content-Type
        System.out.println(response); // 打印

    }

}

MockMvcRequestBuilders

MockMvcRequestBuilders是一个用于构建MockHttpServletRequestBuilder对象的工具类。MockHttpServletRequestBuilder是MockMvc框架中用于构建模拟HTTP请求的请求构建器。 一些常用的MockMvcRequestBuilders方法包括:

get

MockHttpServletRequestBuilder getRequest = MockMvcRequestBuilders.get("/api/users");

post

MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post("/api/users")
        .contentType(MediaType.APPLICATION_JSON)
        .content("{\"username\": \"john\", \"password\": \"123456\"}");

put

MockHttpServletRequestBuilder putRequest = MockMvcRequestBuilders.put("/api/users/1")
        .contentType(MediaType.APPLICATION_JSON)
        .content("{\"username\": \"john\", \"password\": \"654321\"}");

delete

MockHttpServletRequestBuilder deleteRequest = MockMvcRequestBuilders.delete("/api/users/1");

总结

一定要根据自己的实际情况和业务来测试自己的代码,快去试试吧!

作者:乐乐家的乐乐
来源:https://juejin.cn/post/7296373915542601762

 

请登录后发表评论

    没有回复内容