SpringBoot - Test 用法
使用 idea 新建 SpringBoot 项目时,大家应该会发现除了正常的 src/main 文件夹之外,还会有个 test 文件夹,相应的会引入 spring-boot-starter-test 模块, 本文就来聊聊这个 test 模板的用法
说到 test 大家都会想到 junit,是的 spring-boot-starter-test 默认集成的就是 junit,但需要注意的是:springboot2.x 的版本, 默认使用的是 junit5 版本, junit4 和 junit5 两个版本差别比较大,需要注意下用法:
通常我们只要引入 spring-boot-starter-test 依赖就行,它包含了一些常用的模块 Junit、Spring Test、AssertJ、Hamcrest、Mockito 等
为什么使用 JUnit5
- JUnit4 被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5 中支持 lambda 表达式,语法简单且代码不冗余。
- JUnit5 易扩展,包容性强,可以接入其他的测试引擎。
- 功能更强大提供了新的断言机制、参数化测试、重复性测试等新功能。
如无特殊说明,直接使用 junit5 相关 api(org.junit.jupiter.api.*, 这是 junit5 引入的; junit4 引入的是 org.junit.Test 这样类似的包)
引入
- pom.xml 中加入 spring-boot-starter-test 模块(一般会默认引入):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
需要注意的是 SpringBoot2.2 开始会需要排除 junit-vintage-engine,这个是因为早期的 junit5 默认引入了 junit-vintage-engine 用于运行 junit4 测试(junit-jupiter-engine 用于 junit5 测试), 一般新的项目无需 junit4,因此 POM 中的默认依赖项排除在外
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency>
但从 SpringBoot2.4.0 开始 spring-boot-starter-test 中的 junit5 已经默认移除了 junit-vintage-engine,所以就无需手动排除了
当然,如果有写老的测试代码还是使用了 junit4 相关 api 的话:
- SpringBoot 2.2 到 2.4.0 之前,只要将手动排除的代码注释掉即可
- SpringBoot 2.4.0 之后,需要手动添加 junit-vintage-engine:
<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency>
看好实际项目中使用的 SpringBoot 版本
使用
junit5 使用的都是 org.junit.jupiter.xxx
- 在测试类加入 @SpringBootTest:这个注解是 SpringBoot 自 1.4.0 版本开始引入的一个用于测试的注解,这样一般就可以了,不用加 @RunWith(SpringRunner.class),这个是 junit4 的注解
- 在测试方法中加入 @Test: 注意是 org.junit.jupiter.api.Test
import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class MyApplicationTests { @Test void contextLoads() { //xxx } }
右击代码即可运行测试
@DisplayName() 测试显示中文名称
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @DisplayName("测试 1") @SpringBootTest class StandAloneServerApplicationTests { @DisplayName("测试方法 1") @Test void contextLoads() { //xxx } }
指定测试顺序
import org.junit.jupiter.api.*; import org.springframework.boot.test.context.SpringBootTest; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @DisplayName("测试 1") @SpringBootTest class StandAloneServerApplicationTests { @Order(2) @DisplayName("测试方法 1") @Test void contextLoads() { } @Order(1) @DisplayName("测试方法 2") @Test void test2() { } }
更多其他注解用法,可查阅官方文档 writing-tests-annotations
测试 Controller
测试 Controller 不建议直接引用 Controller 类进行测试,因为 Controller 一般是提供 api 给外部访问用的,使用 http 请求更能模拟真实场景,SpringBoot 中提供了 Mockito 可以达到效果
举例
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @DisplayName("用户 Controller 层测试") @AutoConfigureMockMvc //不启动服务器,使用 mockMvc 进行测试 http 请求 @SpringBootTest class UserControllerTest { private final Logger logger = LoggerFactory.getLogger(this.getClass()); MockMvc mockMvc; ObjectMapper objectMapper; @Autowired public UserControllerTest(MockMvc mockMvc, ObjectMapper objectMapper) { this.mockMvc = mockMvc; this.objectMapper = objectMapper; } @Order(1) @DisplayName("注册") @Test void register() throws Exception { UserForm userForm = new UserForm(); userForm.setName("jonesun"); userForm.setAge(30); userForm.setEmail("sunr922@163.com"); //请求路径不要错了 MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/users") //这里要特别注意和 content 传参数的不同,具体看你接口接受的是哪种 // .param("userName",info.getUserName()).param("password",info.getPassword()) //传 json 参数,最后传的形式是 Body = {"password":"admin","userName":"admin"} .content(objectMapper.writeValueAsString(userForm)) .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()) .andReturn(); //得到返回代码 int status = mvcResult.getResponse().getStatus(); //得到返回结果 String content = mvcResult.getResponse().getContentAsString(); logger.info("status: {}, content: {}", status, content); } @Order(2) @DisplayName("列表") @Test void list() throws Exception { RequestBuilder request = MockMvcRequestBuilders.get("/users") // .param("searchPhrase","ABC") //传参 .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON); //请求类型 JSON MvcResult mvcResult = mockMvc.perform(request) // 期望的结果状态 等同于 Assert.assertEquals(200,status); .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()) //添加 ResultHandler 结果处理器,比如调试时 打印结果(print 方法) 到控制台 .andReturn(); //返回验证成功后的 MvcResult;用于自定义验证/下一步的异步处理; int status = mvcResult.getResponse().getStatus(); //得到返回代码 String content = mvcResult.getResponse().getContentAsString(); //得到返回结果 logger.info("status: {}, content: {}", status, content); // // mockMvc.perform(MockMvcRequestBuilders.get("/users")) // .andDo(print()) // .andExpect(status().isOk()) // .andExpect(content().string(containsString("Hello World"))); } }
完整代码见 github
测试并发
有时我们需要对自己编写的代码做并发测试,看在高并发情况下,代码中是否存在线程安全等问题,通常可以利用 Jmeter 或者浏览器提供的各个插件(如 postman)。 实际上我们可以利用 JUC 提供的并发同步器 CountDownLatch 和 Semaphore 来实现
举例,模拟秒杀场景
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @SpringBootTest class GoodsServiceTest { private final Logger logger = LoggerFactory.getLogger(getClass()); @Autowired GoodsService goodsService; public static final Long TEST_GOODS_ID = 123L; @Order(1) @Test void init() { Goods goods = new Goods(TEST_GOODS_ID, "商品 1", 100); goodsService.init(goods); } @Order(2) @Test void buy() { goodsService.buy(TEST_GOODS_ID); } @DisplayName("秒杀单个商品") @Order(3) @Test void batchBuy() throws InterruptedException { Integer inventory = goodsService.getInventoryByGoodsId(TEST_GOODS_ID); logger.info("【{}】准备秒杀, 当前库存: {}", TEST_GOODS_ID, inventory); LocalDateTime startTime = LocalDateTime.now(); AtomicInteger buySuccessAtomicInteger = new AtomicInteger(); AtomicInteger notBoughtAtomicInteger = new AtomicInteger(); AtomicInteger errorAtomicInteger = new AtomicInteger(); final int totalNum = 300; //用于发出开始信号 final CountDownLatch countDownLatchSwitch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(totalNum); //控制并发量 10 50 100 200 Semaphore semaphore = new Semaphore(200); ExecutorService executorService = Executors.newFixedThreadPool(totalNum); for (int i = 0; i < totalNum; i++) { executorService.execute(() -> { try { countDownLatchSwitch.await(); semaphore.acquire(); TimeUnit.SECONDS.sleep(1); boolean result = goodsService.buy(TEST_GOODS_ID); if (result) { buySuccessAtomicInteger.incrementAndGet(); } else { notBoughtAtomicInteger.incrementAndGet(); } } catch (Exception e) { e.printStackTrace(); errorAtomicInteger.incrementAndGet(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatchSwitch.countDown(); countDownLatch.await(); logger.info("测试完成,花费 {}毫秒,【{}】总共{}个用户抢购{}件商品,{}个人买到 {}个人未买到,{}个人发生异常,商品还剩{}个", TEST_GOODS_ID, ChronoUnit.MILLIS.between(startTime, LocalDateTime.now()), totalNum, inventory, buySuccessAtomicInteger.get(), notBoughtAtomicInteger.get(), errorAtomicInteger.get(), goodsService.getInventoryByGoodsId(TEST_GOODS_ID)); assertEquals(inventory, buySuccessAtomicInteger.get()); } }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: SpringCloud 集成使用
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论