SpringBoot 测试实践 - 3:@MockBean、@SpyBean 、提升测试运行速度、Testcontainer

上一节:SpringBoot 测试实践 - 2:单元测试与集成测试 编写测试的时候,我们必须保证外部依赖行为一致,也需要模拟一些边界条件,所以我们需要使用 Mock 来模拟对象的行为。SpringBoot 提供了 @MockBean 和 @SpyBean 注解,可以方便地将模拟对象与 Spring 测试相结合,简化测试代码的编写 @MockBean @MockBean 是 Spring Boot Test提供的注解,用于在 Spring Boot 测试中创建一个模拟的 Bean 实例,并注入到测试类中的依赖项中。使用 Mock 可以控制被 Mock 对象的行为:自定义返回值、抛出指定异常等,模拟各种可能的情况,提高测试的覆盖率 @SpringBootTest @RunWith(SpringRunner.class) public class MyServiceTest { @MockBean private ExternalDependency externalDependency; @Autowired private MyService myService; @Test public void testSomeMethod() { // 定义外部依赖的行为 Mockito.when(externalDependency.someMethod()).thenReturn("Mocked Result"); // 调用被测试类的方法 // 被测方法内部调用了 ExternalDependency 的 someMethod 方法 String result = myService.someMethod(); // 验证外部依赖的方法是否被调用 Mockito.verify(externalDependency).someMethod(); // 断言结果 assertEquals("Mocked Result", result); } } 需要注意的是:使用了 @MockBean,会创建完全模拟的对象,它完全替代了被模拟的 Bean,并且所有方法的调用都被模拟。对于未指定行为的方法,返回值如果是基本类型则返回对应基本类型的默认值,如果是引用类型则返回 null ...

August 24, 2023

SpringBoot 测试实践 - 2:单元测试与集成测试

上一节:SpringBoot 测试实践 - 1:常用的工具 下一节:SpringBoot 测试实践 - 3:@MockBean、@SpyBean 、提升测试运行速度、Testcontainer 单元测试 vs. 集成测试 只编写单测,无法测试方法之间的集成情况,而且某些需求可能会修改多个方法,这可能会影响方法对应的单测,涉及到大量的相关单测的修改,这样的维护成本很高 可以把重心放在完善集成测试上,专注从外部判断程序是否符合预期。对于一些非常重要的方法,增加单元测试可以减轻集成测试排查错误的难度 先导知识可以参考上一节:SpringBoot 测试实践 - 1:常用的工具 SpringBootTest 和 MockMvc 进行集成测试 从 Spring Boot 2.1 开始 @ExtendWith({SpringExtension.class}) 作为元注解包含在 Spring Boot 测试注解中,例如 @DataJpaTest、@WebMvcTest 和 @SpringBootTest,所以我们不用重复添加 @ExtendWith({SpringExtension.class}) 注解 HelloWorld 测试 使用 SpringBoot 一个简单的 HelloWorld 案例,通过 @SpringBootTest 可以在测试环境中加载整个 Spring 应用程序上下文,@SpringBootTest 注解会扫描应用程序的主配置类,并加载所有的 Bean(包括依赖的 Bean)到测试上下文中。这样,测试中就可以使用完整的 Spring 功能,包括依赖注入、AOP、事务管理等 使用 @AutoConfigureMockMvc 自动配置 MockMvc,通过 MockMvc 可以模拟 HTTP 请求,并对响应的结果进行断言和验证 import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class MySpringBootTest { @Autowired private MockMvc mockMvc; // 注入 MockMvc @Test public void testHelloWorld() throws Exception { // 发送 GET 请求 mockMvc.perform(MockMvcRequestBuilders.get("/hello") // 设置请求头 .accept(MediaType.APPLICATION_JSON)) // 验证响应状态码 .andExpect(MockMvcResultMatchers.status().isOk()) // 验证响应内容 .andExpect(MockMvcResultMatchers.content().string("Hello, World!")); } } 涉及数据层的测试:H2 部分操作涉及到数据库,一般都会引入数据层的依赖,在对应的 HTTP 请求后,对响应体和数据库数据进行断言和验证,就像下面这样: ...

August 22, 2023

SpringBoot 测试实践 - 1:常用的工具

下一节:SpringBoot 测试实践 - 2:单元测试与集成测试 我自己接触到的一些商业或是开源的基于 SpringBoot 项目,它们大部分是没有测试代码的,test 文件夹只有脚手架初始化生成的那个测试类,跟不同的开发聊到这个话题,发现他们中的大部分没有写测试的习惯,或者是觉得写测试代码麻烦,主要还是依赖测试工程师做黑盒的测试。只做黑盒测试的话有一定的的局限性,一些边界的条件可能就覆盖不到,而且相对来说人也比较容易出错、遗漏。而测试代码能解决其中很大一部分的问题,利用好单元测试和集成测试在某些情况下相对于直接通过 UI 进行测试是要更方便、节省时间的,所以想通过几篇博客来分享一下自己的测试实践 为什么要写测试(优点) 覆盖更多的边界条件,且随时都可以运行测试代码(一劳永逸) 缩小测试范围:测试某个方法只需要运行对应的测试代码,而不需要运行整个项目通过请求接口进行测试 对重构更友好,可以随时重构有集成测试的代码,不用担心打破原有的代码 其他人也可以通过测试快速地理清楚对应被测代码的主线逻辑(类似文档的作用,特别是复杂代码,通过测试能快速理解上手) 写测试的过程,给自己一个新的视角去审视代码结构的设计,有助于改善代码设计 当然代码方式的测试也并非完美无缺:测试代码增加编写和维护的成本,同时一些外部依赖也需要通过 Mock 的方式实现,这些都提高了整个测试编写的门槛。也倒逼我们思考更好地组织代码,减少依赖 另一个方面:测试对于重构也是至关重要的,随着对业务的理解越来越深刻,可以重构代码,抽象出了一些共性的逻辑,优化代码结构,但是如果没有相关测试,面对着旧代码就只能望而却步了 测试工具:JUnit 5, AssertJ,Mockito spring-boot-starter-test 自带常用的测试工具:JUnit5、Assertj、Mockito,可以直接使用 JUnit5 Junit 5 包含: JUnit Platform:Test Engine Jupiter:编程模型和拓展模型 Vintage:兼容老版本 JUnit 4 和 5 使用的包有所不同 // JUnit 4 import org.junit.Test; import static org.junit.Assert.assertEquals; // JUnit 5 import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; 如果不考虑兼容 JUnit 4 的测试,我们可以直接在依赖中直接排除 JUnit 4 的依赖,这样也可以避免在使用的时候错误地引入 JUnit 4 的包 ...

August 21, 2023

SpringBoot 整合 SpringSecurity 梳理

文档 Spring Security Reference SpringBoot+SpringSecurity+jwt整合及初体验 JSON Web Token 入门教程 - 阮一峰 JWT 官网 SpringSecurity 项目 GitHub 仓库地址:https://github.com/aaronlinv/springsecurity-jwt-demo 依赖 主要用到了: SpringSecurity,Thymeleaf,Web,Lombok <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependency> 页面 编写页面和 Controller 进行测试,具体页面可以看 代码 主要包含了首页(index),订单(order),还有 user,role,menu这三个位于 /system 下,需要 admin 权限 使用内存用户进行表单登录 在 static 下新建 login.html,用于登录 <form action="/login" method="post"> <label for="username">账户</label><input type="text" name="username" id="username"><br> <label for="password">密码</label><input type="password" name="password" id="password"><br> <input type="submit" value="登录"> </form> 编写继承 WebSecurityConfigurerAdapter 的 Security 配置类,并开启 @EnableWebSecurity 注解,这个注解包含了 @Configuration WebSecurityConfigurerAdapter 中有两个方法,它们名称相同,但是入参不同 protected void configure(HttpSecurity http) throws Exception protected void configure(AuthenticationManagerBuilder auth) throws Exception 入参为 HttpSecurity 的 configure 可以配置拦截相关的参数 另一个入参为 AuthenticationManagerBuilder,则是用来配置验证相关的参数 ...

August 24, 2021