美文网首页单元测试
SpringBoot 单元测试与 Mockito 使用

SpringBoot 单元测试与 Mockito 使用

作者: 聪明的奇瑞 | 来源:发表于2019-12-23 23:03 被阅读0次

    SpringBoot 单元测试与 Mockito 使用

    单元测试应遵循 → AIR 原则

    SpringBoot 测试支持由两个模块提供:

    • spring-boot-test 包含核心项目
    • spring-boot-test-autoconfigure 支持测试的自动配置

    通常我们只要引入 spring-boot-starter-test 依赖就行,它包含了一些常用的模块 Junit、Spring Test、AssertJ、Hamcrest、Mockito 等。

    相关注解

    SpringBoot 使用了 Junit4 作为单元测试框架,所以注解与 Junit4 是一致的。

    注解 作用
    @Test(excepted==xx.class,timeout=毫秒数) 修饰一个方法为测试方法,excepted参数可以忽略某些异常类
    @Before 在每一个测试方法被运行前执行一次
    @BeforeClass 在所有测试方法执行前执行
    @After 在每一个测试方法运行后执行一次
    @AfterClass 在所有测试方法执行后执行
    @Ignore 修饰的类或方法会被测试运行器忽略
    @RunWith 更改测试运行器

    @SpringBootTest

    SpringBoot提供了一个 @SpringBootTest 注解用于测试 SpringBoot 应用,它可以用作标准 spring-test @ContextConfiguration 注释的替代方法,其原理是通过 SpringApplication 在测试中创建ApplicationContext。

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ApplicationTest {
    }
    

    该注解提供了两个属性用于配置:

    • webEnvironment:指定Web应用环境,它可以是以下值
      • MOCK:提供一个模拟的 Servlet 环境,内置的 Servlet 容器没有启动,配合可以与@AutoConfigureMockMvc 结合使用,用于基于 MockMvc 的应用程序测试。
      • RANDOM_PORT:加载一个 EmbeddedWebApplicationContext 并提供一个真正嵌入式的 Servlet 环境,随机端口。
      • DEFINED_PORT:加载一个 EmbeddedWebApplicationContext 并提供一个真正嵌入式的 Servlet 环境,默认端口 8080 或由配置文件指定。
      • NONE:使用 SpringApplication 加载 ApplicationContext,但不提供任何 servlet 环境。
    • classes:指定应用启动类,通常情况下无需设置,因为 SpringBoot 会自动搜索,直到找到 @SpringBootApplication 或 @SpringBootConfiguration 注解。

    单元测试回滚

    如果你添加了 @Transactional 注解,它会在每个测试方法结束时会进行回滚操作。

    但是如果使用 RANDOM_PORT 或 DEFINED_PORT 这种真正的 Servlet 环境,HTTP 客户端和服务器将在不同的线程中运行,从而分离事务。 在这种情况下,在服务器上启动的任何事务都不会回滚。

    断言

    JUnit4 结合 Hamcrest 提供了一个全新的断言语法——assertThat,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。

    // 一般匹配符
    int s = new C().add(1, 1);
    // allOf:所有条件必须都成立,测试才通过
    assertThat(s, allOf(greaterThan(1), lessThan(3)));
    // anyOf:只要有一个条件成立,测试就通过
    assertThat(s, anyOf(greaterThan(1), lessThan(1)));
    // anything:无论什么条件,测试都通过
    assertThat(s, anything());
    // is:变量的值等于指定值时,测试通过
    assertThat(s, is(2));
    // not:和is相反,变量的值不等于指定值时,测试通过
    assertThat(s, not(1));
    
    // 数值匹配符
    double d = new C().div(10, 3);
    // closeTo:浮点型变量的值在3.0±0.5范围内,测试通过
    assertThat(d, closeTo(3.0, 0.5));
    // greaterThan:变量的值大于指定值时,测试通过
    assertThat(d, greaterThan(3.0));
    // lessThan:变量的值小于指定值时,测试通过
    assertThat(d, lessThan(3.5));
    // greaterThanOrEuqalTo:变量的值大于等于指定值时,测试通过
    assertThat(d, greaterThanOrEqualTo(3.3));
    // lessThanOrEqualTo:变量的值小于等于指定值时,测试通过
    assertThat(d, lessThanOrEqualTo(3.4));
    
    // 字符串匹配符
    String n = new C().getName("Magci");
    // containsString:字符串变量中包含指定字符串时,测试通过
    assertThat(n, containsString("ci"));
    // startsWith:字符串变量以指定字符串开头时,测试通过
    assertThat(n, startsWith("Ma"));
    // endsWith:字符串变量以指定字符串结尾时,测试通过
    assertThat(n, endsWith("i"));
    // euqalTo:字符串变量等于指定字符串时,测试通过
    assertThat(n, equalTo("Magci"));
    // equalToIgnoringCase:字符串变量在忽略大小写的情况下等于指定字符串时,测试通过
    assertThat(n, equalToIgnoringCase("magci"));
    // equalToIgnoringWhiteSpace:字符串变量在忽略头尾任意空格的情况下等于指定字符串时,测试通过
    assertThat(n, equalToIgnoringWhiteSpace(" Magci   "));
    
    // 集合匹配符
    List<String> l = new C().getList("Magci");
    // hasItem:Iterable变量中含有指定元素时,测试通过
    assertThat(l, hasItem("Magci"));
    
    Map<String, String> m = new C().getMap("mgc", "Magci");
    // hasEntry:Map变量中含有指定键值对时,测试通过
    assertThat(m, hasEntry("mgc", "Magci"));
    // hasKey:Map变量中含有指定键时,测试通过
    assertThat(m, hasKey("mgc"));
    // hasValue:Map变量中含有指定值时,测试通过
    assertThat(m, hasValue("Magci"));
    

    基本的单元测试例子

    下面是一个基本的单元测试例子,对某个方法的返回结果进行断言:

    @Service
    public class UserService {
    
        public String getName() {
            return "lyTongXue";
        }
        
    }
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserServiceTest {
    
        @Autowired
        private UserService service;
    
        @Test
        public void getName() {
            String name = service.getName();
            assertThat(name,is("lyTongXue"));
        }
    
    }
    

    Controller 测试

    Spring 提供了 MockMVC 用于支持 RESTful 风格的 Spring MVC 测试,使用 MockMvcBuilder 来构造MockMvc 实例。MockMvc 有两个实现:

    • StandaloneMockMvcBuilder:指定 WebApplicationContext,它将会从该上下文获取相应的控制器并得到相应的 MockMvc

      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class UserControllerTest  {
          @Autowired
          private WebApplicationContext webApplicationContext;
          private MockMvc mockMvc;
          @Before
          public void setUp() throws Exception {
              mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
      } 
      
    • DefaultMockMvcBuilder:通过参数指定一组控制器,这样就不需要从上下文获取了

      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class UserControllerTest  {
          private MockMvc mockMvc;
          @Before
          public void setUp() throws Exception {
              mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
          } 
      }    
      

    下面是一个简单的用例,对 UserController 的 /v1/users/{id} 接口进行测试。

    @RestController
    @RequestMapping("v1/users")
    public class UserController {
    
        @GetMapping("/{id}")
        public User get(@PathVariable("id") String id) {
            return new User(1, "lyTongXue");
        }
    
        @Data
        @AllArgsConstructor
        public class User {
            private Integer id;
            private String name;
        }
    
    }
    
    // ...
    import static org.hamcrest.Matchers.containsString;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest {
    
        @Autowired
        private WebApplicationContext webApplicationContext;
        private MockMvc mockMvc;
    
        @Before
        public void setUp() {
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        }
    
        @Test
        public void getUser() {
            mockMvc.perform(get("/v1/users/1")
                    .accept(MediaType.APPLICATION_JSON_UTF8))
                    .andExpect(status().isOk())
               .andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
        }
      
    }
    

    方法描述

    • perform:执行一个 RequestBuilder 请求,返回一个 ResultActions 实例对象,可对请求结果进行期望与其它操作

    • get:声明发送一个 get 请求的方法,更多的请求类型可查阅→MockMvcRequestBuilders 文档

    • andExpect:添加 ResultMatcher 验证规则,验证请求结果是否正确,验证规则可查阅→MockMvcResultMatchers 文档

    • andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台,更多处理器可查阅→MockMvcResultHandlers 文档

    • andReturn:返回执行请求的结果,该结果是一个恩 MvcResult 实例对象→MvcResult 文档

    Mock 数据

    在单元测试中,Service 层的调用往往涉及到对数据库、中间件等外部依赖。而在单元测试 AIR 原则中,单元测试应该是可以重复执行的,不应受到外界环境的影响的。此时我们可以通过 Mock 一个实现来处理这种情况。

    如果不需要对静态方法,私有方法等特殊进行验证测试,则仅仅使用 Spring boot 自带的 Mockito 即可完成相关的测试数据 Mock。若需要则可以使用 PowerMock,简单实用,结合 Spring 可以使用注解注入。

    @MockBean

    SpringBoot 在执行单元测试时,会将该注解的 Bean 替换掉 IOC 容器中原生 Bean。

    例如下面代码中, ProjectService 中通过 ProjectMapper 的 selectById 方法进行数据库查询操作:

    @Service
    public class ProjectService {
    
        @Autowired
        private ProjectMapper mapper;
    
        public ProjectDO detail(String id) {
            return mapper.selectById(id);
        }
    
    }
    
    

    此时我们可以对 Mock 一个 ProjectMapper 对象替换掉 IOC 容器中原生的 Bean,来模拟数据库查询操作,如:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ProjectServiceTest {
      
        @MockBean
        private ProjectMapper mapper;
        @Autowired
        private ProjectService service;
    
        @Test
        public void detail() {
            ProjectDemoDO model = new ProjectDemoDO();
            model.setId("1");
            model.setName("dubbo-demo");
            Mockito.when(mapper.selectById("1")).thenReturn(model);
            ProjectDemoDO entity = service.detail("1");
            assertThat(entity.getName(), containsString("dubbo-demo"));
        }
    
    }
    
    

    Mockito 常用方法

    Mockito 更多的使用可查看→官方文档

    mock() 对象
    List list = mock(List.class);
    
    
    verify() 验证互动行为
    @Test
    public void mockTest() {
        List list = mock(List.class);
      list.add(1);
      // 验证 add(1) 互动行为是否发生
      Mockito.verify(list).add(1);
    }
    
    
    when() 模拟期望结果
    @Test
    public void mockTest() {
      List list = mock(List.class);
      when(mock.get(0)).thenReturn("hello");
      assertThat(mock.get(0),is("hello"));
    }
    
    
    doThrow() 模拟抛出异常
    @Test(expected = RuntimeException.class)
    public void mockTest(){
      List list = mock(List.class);
      doThrow(new RuntimeException()).when(list).add(1);
      list.add(1);
    }
    
    
    @Mock 注解

    在上面的测试中我们在每个测试方法里都 mock 了一个 List 对象,为了避免重复的 mock,使测试类更具有可读性,我们可以使用下面的注解方式来快速模拟对象:

    // @RunWith(MockitoJUnitRunner.class) 
    public class MockitoTest {
        @Mock
        private List list;
    
        public MockitoTest(){
            // 初始化 @Mock 注解
            MockitoAnnotations.initMocks(this);
        }
    
        @Test
        public void shorthand(){
            list.add(1);
            verify(list).add(1);
        }
    }
    
    
    when() 参数匹配
    @Test
    public void mockTest(){
        Comparable comparable = mock(Comparable.class);
      //预设根据不同的参数返回不同的结果
      when(comparable.compareTo("Test")).thenReturn(1);
      when(comparable.compareTo("Omg")).thenReturn(2);
      assertThat(comparable.compareTo("Test"),is(1));
      assertThat(comparable.compareTo("Omg"),is(2));
      //对于没有预设的情况会返回默认值
       assertThat(list.get(1),is(999));
       assertThat(comparable.compareTo("Not stub"),is(0));
    }
    
    
    Answer 修改对未预设的调用返回默认期望
    @Test
    public void mockTest(){
      //mock对象使用Answer来对未预设的调用返回默认期望值
      List list = mock(List.class,new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
          return 999;
        }
      });
      //下面的get(1)没有预设,通常情况下会返回NULL,但是使用了Answer改变了默认期望值
      assertThat(list.get(1),is(999));
      //下面的size()没有预设,通常情况下会返回0,但是使用了Answer改变了默认期望值
      assertThat(list.size(),is(999));
    }
    
    
    spy() 监控真实对象

    Mock 不是真实的对象,它只是创建了一个虚拟对象,并可以设置对象行为。而 Spy是一个真实的对象,但它可以设置对象行为。

    @Test(expected = IndexOutOfBoundsException.class)
    public void mockTest(){
      List list = new LinkedList();
      List spy = spy(list);
      //下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
      when(spy.get(0)).thenReturn(3);
      //使用doReturn-when可以避免when-thenReturn调用真实对象api
      doReturn(999).when(spy).get(999);
      //预设size()期望值
      when(spy.size()).thenReturn(100);
      //调用真实对象的api
      spy.add(1);
      spy.add(2);
      assertThat(spy.size(),is(100));
      assertThat(spy.size(),is(1));
      assertThat(spy.size(),is(2));
      verify(spy).add(1);
      verify(spy).add(2);
      assertThat(spy.get(999),is(999));
    }
    
    
    reset() 重置 mock
    @Test
    public void reset_mock(){
      List list = mock(List.class);
      when(list.size()).thenReturn(10);
      list.add(1);
        assertThat(list.size(),is(10));
      //重置mock,清除所有的互动和预设
      reset(list);
      assertThat(list.size(),is(0));
    }
    
    
    times() 验证调用次数
    @Test
    public void verifying_number_of_invocations(){
      List list = mock(List.class);
      list.add(1);
      list.add(2);
      list.add(2);
      list.add(3);
      list.add(3);
      list.add(3);
      //验证是否被调用一次,等效于下面的times(1)
      verify(list).add(1);
      verify(list,times(1)).add(1);
      //验证是否被调用2次
      verify(list,times(2)).add(2);
      //验证是否被调用3次
      verify(list,times(3)).add(3);
      //验证是否从未被调用过
      verify(list,never()).add(4);
      //验证至少调用一次
      verify(list,atLeastOnce()).add(1);
      //验证至少调用2次
      verify(list,atLeast(2)).add(2);
      //验证至多调用3次
      verify(list,atMost(3)).add(3);
    }
    
    
    inOrder() 验证执行顺序
    @Test
    public void verification_in_order(){
      List list = mock(List.class);
      List list2 = mock(List.class);
      list.add(1);
      list2.add("hello");
      list.add(2);
      list2.add("world");
      //将需要排序的mock对象放入InOrder
      InOrder inOrder = inOrder(list,list2);
      //下面的代码不能颠倒顺序,验证执行顺序
      inOrder.verify(list).add(1);
      inOrder.verify(list2).add("hello");
      inOrder.verify(list).add(2);
      inOrder.verify(list2).add("world");
    }
    
    
    verifyZeroInteractions() 验证零互动行为
     @Test
     public void mockTest(){
       List list = mock(List.class);
       List list2 = mock(List.class);
       List list3 = mock(List.class);
       list.add(1);
       verify(list).add(1);
       verify(list,never()).add(2);
       //验证零互动行为
       verifyZeroInteractions(list2,list3);
     }
    
    
    verifyNoMoreInteractions() 验证冗余互动行为
    @Test(expected = NoInteractionsWanted.class)
    public void mockTest(){
      List list = mock(List.class);
      list.add(1);
      list.add(2);
      verify(list,times(2)).add(anyInt());
      //检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到,所以下面的代码会通过
      verifyNoMoreInteractions(list);
    
      List list2 = mock(List.class);
      list2.add(1);
      list2.add(2);
      verify(list2).add(1);
      //检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
      verifyNoMoreInteractions(list2);
    }
    
    

    相关文章

      网友评论

        本文标题:SpringBoot 单元测试与 Mockito 使用

        本文链接:https://www.haomeiwen.com/subject/piotoctx.html