前言
使用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模块(一般会默认引入):
1 2 3 4 5
| <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中的默认依赖项排除在外
1 2 3 4 5 6 7 8 9 10 11
| <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:
1 2 3 4 5 6 7 8 9 10 11
| <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
1 2 3 4 5 6 7 8 9 10 11 12 13
| import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest class MyApplicationTests {
@Test void contextLoads() { }
}
|
右击代码即可运行测试
@DisplayName()测试显示中文名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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() { }
}
|
指定测试顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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可以达到效果
举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @DisplayName("用户Controller层测试") @AutoConfigureMockMvc @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(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")
.accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON); MvcResult mvcResult = mockMvc.perform(request) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()) .andReturn(); int status = mvcResult.getResponse().getStatus(); String content = mvcResult.getResponse().getContentAsString(); logger.info("status: {}, content: {}", status, content);
} }
|
完整代码见 github
测试并发
有时我们需要对自己编写的代码做并发测试,看在高并发情况下,代码中是否存在线程安全等问题,通常可以利用Jmeter或者浏览器提供的各个插件(如postman)。
实际上我们可以利用JUC提供的并发同步器CountDownLatch和Semaphore来实现
举例,模拟秒杀场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| @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);
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()); }
}
|
完整代码见 github
– 未完,抽时间继续整理整理 –