SpringBoot Test及注解详解(含Mockito)

SpringBoot Test及注解详解(含Mockito)一 版本差异 SpringBoot2 2 0 版本开始引入 JUnit5 作为单元测试默认库 在 SpringBoot2 2 0 版本之前 spring boot starter test 包含了 JUnit4 的依赖 SpringBoot2 2 0 版本之后替换成了 JunitJupiter pom xml dependency groupId org springframew boot groupId dependency

一、版本差异

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,在 Spring Boot 2.2.0 版本之前,spring-boot-starter-test 包含了 JUnit 4 的依赖,Spring Boot 2.2.0 版本之后替换成了 Junit Jupiter。

  • pom.xml
 <dependency> <groupId>org.springframework.boot 
     groupId> <artifactId>spring-boot-starter-test 
      artifactId> <scope>test 
       scope>  
        dependency> 
  • JUnit 5: Java单元测试框架
  • Spring Test & Spring Boot Test: Spring Boot的测试工具和支持
  • AssertJ: 流式断言
  • Hamcrest: Hamcrest断言
  • Mockito: Java Mock框架
  • JSONassert: JSON断言
  • JsonPath: XPath for JSON

二、SpringBootTest和Junit5的使用

整体上,Spring Boot Test支持的测试种类,大致可以分为如下三类

  1. 单元测试:一般面向方法,编写一般业务代码时,测试成本较大。涉及到的注解有@Test。
  2. 切片测试:一般面向难于测试的边界功能,介于单元测试和功能测试之间。涉及到的注解有 @WebMvcTest等。主要就是对于Controller的测试,分离了Service层,这里就涉及到Moc控制层所依赖的组件了
  3. 功能测试:一般面向某个完整的业务功能,同时也可以使用切面测试中的mock能力,推荐使用。涉及到的注解有@SpringBootTest等。
  1. 单元测试

集成测试,不启动server,以创建项目后自动生成的默认测试类为例:

@SpringBootTest class TestDemoApplicationTests { 
    @Test void contextLoads() { 
    } } 

默认无参数的@SpringBootTest 注解会加载一个Web Application Context并提供Mock Web Environment,但是不会启动内置的server。这点从日志中没有打印Tomcat started on port(s)可以佐证。

  1. 集成测试,启动server

新建一个测试类如下:

//指定@SpringBootTest的Web Environment为RANDOM_PORT //此时,将会加载Applicaiton Context,并启动server,server侦听在随机端口上。在测试类中通过@LocalServerPort获取该端口值。 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class DemoTest { 
    @LocalServerPort private Integer port; @Test @DisplayName("should access application") void shouldAccessApplication() { 
    assertThat(port).isGreaterThan(1024); } } 

也可以通过指定@SpringBootTest的Web Environment为DEFINED_PORT 来指定server侦听应用程序配置的端口,默认为8080。不过这种指定端口的方式很少使用,因为如果本地同时启动应用时,会导致端口冲突

  1. 更多关系JUnit5集成SpringBootTest的例子,参考这个文档,我这里不在啰嗦

三、Spring Boot Test中的主要注解

  1. 在说Mockito之前,先看一下SpringBootTest的注解,Mockito是一个独立的框架,被springboot集成了而已。

从功能上讲,Spring Boot Test中的注解主要分如下几类

在这里插入图片描述

  • 配置类型的注解:

在这里插入图片描述

使用@SpringBootApplication启动测试或者生产代码,被@TestComponent描述的Bean会自动被排除掉。如果不是则需要向@SpringBootApplication添加TypeExcludeFilter。

  • mock类型的注解
    在这里插入图片描述
    @MockBean和@SpyBean这两个注解,在mockito框架中本来已经存在,且功能基本相同。Spring Boot Test又定义一份重复的注解,目的在于使MockBean和SpyBean被ApplicationContext管理,从而方便使用。




MockBean和SpyBean功能非常相似,都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码。

  • 自动配置类型的注解(@AutoConfigure*)
    在这里插入图片描述

这些注解可以搭配@\*Test使用用于开启在@\*Test中未自动配置的功能。例如@SpringBootTest和@AutoConfigureMockMvc组合后,就可以注入org.springframework.test.web.servlet.MockMvc。

“自动配置类型”有两种使用方式:

  1. 在功能测试(即使用@SpringBootTest)时显示添加。
  2. 一般在切片测试中被隐式使用,例如@WebMvcTest注解时,隐式添加了@AutoConfigureCache、@AutoConfigureWebMvc、@AutoConfigureMockMvc。
  • 启动测试类型的注解

所有的@*Test注解都被@BootstrapWith注解,它们可以启动ApplicationContext,是测试的入口,所有的测试类必须声明一个@*Test注解。

在这里插入图片描述

除了@SpringBootTest之外的注解都是用来进行切面测试的,他们会默认导入一些自动配置,点击官方docs查看详情。一般情况下,推荐使用@SpringBootTest而非其它切片测试的注解,简单有效。若某次改动仅涉及特定切片,可以考虑使用切片测试。SpringBootTest是这些注解中最常用的一个,其中包含的配置项如下:

在这里插入图片描述
webEnvironment详细说明:
在这里插入图片描述




  • 相似注解的区别和联系
  • @TestComment vs @Comment
    @TestComponent是另一种@Component,在语义上用来指定某个Bean是专门用于测试的。使用@SpringBootApplication服务时,@TestComponent会被自动排除

  • @TestConfiguration vs @Configuration
    @TestConfiguration是Spring Boot Boot Test提供的,@Configuration是Spring Framework提供的。@TestConfiguration实际上是也是一种@TestComponent,只是这个@TestComponent专门用来做配置用。
    @TestConfiguration和@Configuration不同,它不会阻止@SpringBootTest的查找机制,相当于是对既有配置的补充或覆盖。




  • @SpringBootTest vs @WebMvcTest(或@*Test)
    都可以启动Spring的ApplicationContext @SpringBootTest自动侦测并加载@SpringBootApplication或@SpringBootConfiguration中的配置,@WebMvcTest不侦测配置,只是默认加载一些自动配置。
    @SpringBootTest测试范围一般比@WebMvcTest大。




  • @MockBean vs @SpyBean
    都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码

参考文章

四、Mockito的使用

  1. 简单的一个例子
public class MyMockitoTest { 
    private static UserServiceImpl mockUserService; private static List<String> mockedList; @BeforeAll public static void beforeMock() throws Exception { 
    //使用Mock,模拟UserServiceImpl对象 mockUserService = mock(UserServiceImpl.class); // mock creation 创建mock对象 mockedList = mock(List.class); /* * 默认情况下,所有的函数都有返回值。mock函数默认返回的是null, * 一个空的集合或者一个被对象类型包装的内置类型, * 例如0、false对应的对象类型为Integer、Boolean */ //做一些测试桩(stubbing),也即是定义行为,如果是getOneUser(3),则返回的是null,2则抛出异常 when(mockUserService.getOneUser(1)).thenReturn(new User("a",1)); //注意该抛出异常的stubbing,一定是UserServiceImpl真的有抛出这个异常,Mockito才能编译通过,并执行 when(mockUserService.getOneUser(2)).thenThrow(new IllegalAccessException()); when(mockUserService.getOneUser(3)).thenReturn(new User("a",1)); when(mockUserService.update(isA(User.class))).thenReturn(true); } @Test @DisplayName("GetOneUser") public void testGet() throws Exception { 
    //使用mock模拟出来的mockUserService进行操作 User user = mockUserService.getOneUser(1); User oneUser = mockUserService.getOneUser(2); User oneUser1 = mockUserService.getOneUser(3); System.out.println(user); System.out.println(oneUser); mockUserService.update(user); //验证是否执行过一次getOneUser(1) verify(mockUserService, times(1)).getOneUser(eq(1)); //验证是否执行过一次update verify(mockUserService, times(1)).update(isA(User.class)); } @Test public void testMatcher(){ 
    //使用内置的anyInt()参数匹配器,也可以使用自定义的参数处理器 when(mockedList.get(anyInt())).thenReturn("element"); System.out.println(mockedList.get(999)); } //验证函数的确切、最少、从未调用次数 @Test @DisplayName("testUsingTime") public void testUsingTime(){ 
    //using mock mockedList.add("once"); mockedList.add("twice"); mockedList.add("twice"); mockedList.add("three times"); mockedList.add("three times"); mockedList.add("three times"); // 下面的两个验证函数效果一样,因为verify默认验证的就是times(1) // verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。 verify(mockedList).add("once"); verify(mockedList, times(1)).add("once"); // 验证具体执行次数 verify(mockedList, times(2)).add("twice"); verify(mockedList, times(3)).add("three times"); // 使用never()进行验证,never相当于times(0) verify(mockedList, never()).add("never happened"); // 使用atLeast()/atMost() verify(mockedList, atLeastOnce()).add("three times"); verify(mockedList, atLeast(2)).add("five times"); verify(mockedList, atMost(5)).add("three times"); } } 
  1. 主要看一下使用mockito进行切面测试(Controller)
public class Keywords implements Serializable { 
    private Integer id; private String keyword; private String notes; public Keywords(){ 
   } @Override public String toString() { 
    return "Keywords{" + "id=" + id + ", keyword='" + keyword + '\'' + ", notes='" + notes + '\'' + '}'; } public Integer getId() { 
    return id; } public String getKeyword() { 
    return keyword; } public String getNotes() { 
    return notes; } private Keywords(Builder builder){ 
    this.id=builder.id; this.keyword = builder.keyword; this.notes = builder.notes; } public static class Builder{ 
    private Integer id; private String keyword; private String notes; public Builder setId(Integer id) { 
    this.id = id; return this; } public Builder setKeyword(String keyword) { 
    this.keyword = keyword; return this; } public Builder setNotes(String notes) { 
    this.notes = notes; return this; } public Keywords build(){ 
    return new Keywords(this); } } } 
@Controller public class KeywordController { 
    @Autowired private KeywordsService keywordsService; @Autowired private KeywordsServiceImpl keywordsServiceImpl; @GetMapping(value = "/api/keywords") public Keywords findKeywordById(@RequestParam(value = "id") Integer id) { 
    return keywordsService.findKeywordById(id); } @PostMapping("/api/add") @ResponseBody public Boolean addOne(@RequestBody Keywords keywords){ 
    //调用被spy注解的类的方法,就会直接使用真实的方法 return keywordsServiceImpl.addOne(keywords); } } 
@Repository public interface KeywordsService { 
    Keywords findKeywordById(int i); Boolean addOne(Keywords keywords); } 
@Service public class KeywordsServiceImpl implements KeywordsService { 
    @Override public Keywords findKeywordById(int i) { 
    return null; } @Override public Boolean addOne(Keywords keywords) { 
    System.out.println("invoke spy class method"); System.out.println(keywords); return false; } } 
public class MvcMockitoTest { 
    //mockito 会将 @Mock、@Spy 修饰的对象自动注入到 @InjectMocks 修饰的对象中 //定义MockMvc对象 protected MockMvc mockMvc; @Mock //要mock被测类中依赖的对象使用@Mock注解 private KeywordsService keywordsService; @Spy //被 spy 的对象,调用其方法时默认会走真实方法。 private KeywordsServiceImpl keywordsServiceImpl; @InjectMocks //被测类本身使用@InjectMocks注解 private KeywordController controller; @BeforeEach() public void setup() { 
    MockitoAnnotations.openMocks(this); //初始化MockMvc对象,将KeywordController加载进Spring容器 mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } @Test @DisplayName("findKeywordByIdTest") public void findKeywordByIdTest() throws Exception { 
    Keywords keywords = new Builder().setId(666).setKeyword("tester").setNotes("notes").build(); //打桩,当执行findKeywordById(1)时,就返回上面创建的keywords对象 Mockito.when(keywordsService.findKeywordById(1)).thenReturn(keywords); //执行一个RequestBuider请求,自动执行SpringMvc的流程并映射到相应的控制器执行处理 MvcResult mvcResult = mockMvc.perform( MockMvcRequestBuilders.get("/api/keywords?id=1") //请求的url,请求的方法是Get .contentType(MediaType.APPLICATION_JSON)) //数据的格式 //添加ResultMatcher验证规则,验证perform执行完成后的结果是否正确(对返回的数据进行判断) .andExpect(status().isOk()) //期待的返回状态是200 //添加ResultHandler结果处理器,比如调试打印结果到控制台print() .andDo(print())//打印出请求和相应的内容 //最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理 .andReturn(); System.out.println(mvcResult.getResponse().getContentAsString()); } @Test @DisplayName("addOne") public void testAddOne() throws Exception { 
    Keywords build = new Builder().setId(1).setKeyword("addOne").setNotes("testAddOne").build(); Gson gson = new Gson(); String jsonString =gson.toJson(build); System.out.println(jsonString); MvcResult mvcResult = mockMvc.perform( MockMvcRequestBuilders.post("/api/add") .contentType(MediaType.APPLICATION_JSON)//发送的文本格式 .content(jsonString) .accept(MediaType.APPLICATION_JSON)//接受的文本格式 ) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()).andReturn(); int status = mvcResult.getResponse().getStatus(); assertEquals(status,200); System.out.println("输出 " + mvcResult.getResponse().getContentAsString()); } } 

结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述




mockito可以配合junit5的断言功能使用。更多用法可以参考官方文档

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/227960.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午8:07
下一篇 2026年3月16日 下午8:07


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号