SpringBoot - Test 用法

发布于 2024-10-25 09:30:59 字数 9656 浏览 6 评论 0

使用 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 等

junit5vsjunit4

为什么使用 JUnit5

  • JUnit4 被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5 中支持 lambda 表达式,语法简单且代码不冗余。
  • JUnit5 易扩展,包容性强,可以接入其他的测试引擎。
  • 功能更强大提供了新的断言机制、参数化测试、重复性测试等新功能。

如无特殊说明,直接使用 junit5 相关 api(org.junit.jupiter.api.*, 这是 junit5 引入的; junit4 引入的是 org.junit.Test 这样类似的包)

引入

  1. 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

  1. 在测试类加入 @SpringBootTest:这个注解是 SpringBoot 自 1.4.0 版本开始引入的一个用于测试的注解,这样一般就可以了,不用加 @RunWith(SpringRunner.class),这个是 junit4 的注解
  2. 在测试方法中加入 @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
  }

}

displayName

指定测试顺序

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
24 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文