springboot单元测试

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。

1 @RunWith(SpringRunner.class)
2 @SpringBootTest
3 public class ApplicationTest {
4 }

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

  • 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"))

基本的单元测试例子

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

1 @Service
2 public class UserService {
3 
4     public String getName() {
5         return "lyTongXue";
6     }
7     
8 }
 1 @RunWith(SpringRunner.class)
 2 @SpringBootTest
 3 public class UserServiceTest {
 4 
 5     @Autowired
 6     private UserService service;
 7 
 8     @Test
 9     public void getName() {
10         String name = service.getName();
11         assertThat(name,is("lyTongXue"));
12     }
13 
14 }

Controller 测试

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

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

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

    1 @RunWith(SpringRunner.class)
    2 public class UserControllerTest  {
    3     private MockMvc mockMvc;
    4     @Before
    5     public void setUp() throws Exception {
    6         mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    7     } 
    8 } 

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

 1 @RestController
 2 @RequestMapping("v1/users")
 3 public class UserController {
 4 
 5     @GetMapping("/{id}")
 6     public User get(@PathVariable("id") String id) {
 7         return new User(1, "lyTongXue");
 8     }
 9 
10     @Data
11     @AllArgsConstructor
12     public class User {
13         private Integer id;
14         private String name;
15     }
16 
17 }
 1 // ...
 2 import static org.hamcrest.Matchers.containsString;
 3 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 4 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
 5 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 6 
 7 @RunWith(SpringRunner.class)
 8 @SpringBootTest
 9 public class UserControllerTest {
10 
11     @Autowired
12     private WebApplicationContext webApplicationContext;
13     private MockMvc mockMvc;
14 
15     @Before
16     public void setUp() {
17         mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
18     }
19 
20     @Test
21     public void getUser() {
22         mockMvc.perform(get("/v1/users/1")
23                 .accept(MediaType.APPLICATION_JSON_UTF8))
24                 .andExpect(status().isOk())
25            .andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
26     }
27   
28 }
 

方法描述

  • 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 方法进行数据库查询操作:

 1 @Service
 2 public class ProjectService {
 3 
 4     @Autowired
 5     private ProjectMapper mapper;
 6 
 7     public ProjectDO detail(String id) {
 8         return mapper.selectById(id);
 9     }
10 
11 }

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

 1 @RunWith(SpringRunner.class)
 2 @SpringBootTest
 3 public class ProjectServiceTest {
 4   
 5     @MockBean
 6     private ProjectMapper mapper;
 7     @Autowired
 8     private ProjectService service;
 9 
10     @Test
11     public void detail() {
12         ProjectDemoDO model = new ProjectDemoDO();
13         model.setId("1");
14         model.setName("dubbo-demo");
15         Mockito.when(mapper.selectById("1")).thenReturn(model);
16         ProjectDemoDO entity = service.detail("1");
17         assertThat(entity.getName(), containsString("dubbo-demo"));
18     }
19 
20 }

Mockito 常用方法

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

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

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

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

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

 1 @Test(expected = IndexOutOfBoundsException.class)
 2 public void mockTest(){
 3   List list = new LinkedList();
 4   List spy = spy(list);
 5   //下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
 6   when(spy.get(0)).thenReturn(3);
 7   //使用doReturn-when可以避免when-thenReturn调用真实对象api
 8   doReturn(999).when(spy).get(999);
 9   //预设size()期望值
10   when(spy.size()).thenReturn(100);
11   //调用真实对象的api
12   spy.add(1);
13   spy.add(2);
14   assertThat(spy.size(),is(100));
15   assertThat(spy.size(),is(1));
16   assertThat(spy.size(),is(2));
17   verify(spy).add(1);
18   verify(spy).add(2);
19   assertThat(spy.get(999),is(999));
20 }
reset() 重置 mock
 1 @Test
 2 public void reset_mock(){
 3   List list = mock(List.class);
 4   when(list.size()).thenReturn(10);
 5   list.add(1);
 6     assertThat(list.size(),is(10));
 7   //重置mock,清除所有的互动和预设
 8   reset(list);
 9   assertThat(list.size(),is(0));
10 }
times() 验证调用次数
 1 @Test
 2 public void verifying_number_of_invocations(){
 3   List list = mock(List.class);
 4   list.add(1);
 5   list.add(2);
 6   list.add(2);
 7   list.add(3);
 8   list.add(3);
 9   list.add(3);
10   //验证是否被调用一次,等效于下面的times(1)
11   verify(list).add(1);
12   verify(list,times(1)).add(1);
13   //验证是否被调用2次
14   verify(list,times(2)).add(2);
15   //验证是否被调用3次
16   verify(list,times(3)).add(3);
17   //验证是否从未被调用过
18   verify(list,never()).add(4);
19   //验证至少调用一次
20   verify(list,atLeastOnce()).add(1);
21   //验证至少调用2次
22   verify(list,atLeast(2)).add(2);
23   //验证至多调用3次
24   verify(list,atMost(3)).add(3);
25 }
inOrder() 验证执行顺序
 1 @Test
 2 public void verification_in_order(){
 3   List list = mock(List.class);
 4   List list2 = mock(List.class);
 5   list.add(1);
 6   list2.add("hello");
 7   list.add(2);
 8   list2.add("world");
 9   //将需要排序的mock对象放入InOrder
10   InOrder inOrder = inOrder(list,list2);
11   //下面的代码不能颠倒顺序,验证执行顺序
12   inOrder.verify(list).add(1);
13   inOrder.verify(list2).add("hello");
14   inOrder.verify(list).add(2);
15   inOrder.verify(list2).add("world");
16 }
verifyZeroInteractions() 验证零互动行为
 1  @Test
 2  public void mockTest(){
 3    List list = mock(List.class);
 4    List list2 = mock(List.class);
 5    List list3 = mock(List.class);
 6    list.add(1);
 7    verify(list).add(1);
 8    verify(list,never()).add(2);
 9    //验证零互动行为
10    verifyZeroInteractions(list2,list3);
11  }
verifyNoMoreInteractions() 验证冗余互动行为
 1 @Test(expected = NoInteractionsWanted.class)
 2 public void mockTest(){
 3   List list = mock(List.class);
 4   list.add(1);
 5   list.add(2);
 6   verify(list,times(2)).add(anyInt());
 7   //检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到,所以下面的代码会通过
 8   verifyNoMoreInteractions(list);
 9 
10   List list2 = mock(List.class);
11   list2.add(1);
12   list2.add(2);
13   verify(list2).add(1);
14   //检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
15   verifyNoMoreInteractions(list2);
16 }
 
 注:
1. 如果使用异步的servlet,不能用StandaloneMockMvcBuilder方式进行测试,AsyncContext.getResponse得出的response是一个null,即使加上@SpringTest加载了上下文也是这样
2.@SpringBootTest会开启模拟容器来模拟正式运行,所以会加载相关注解(component)和配置
3.可以使用maven-surefire-plugin的<argLine>或者mvn test -DargLine在测试的时候jvm启动参数或者增加启动命令,比如我的程序执行需要用javaagent去修改字节码,则可以使用:
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <argLine>-javaagent:xxxx xxx.jar</argLine>
                </configuration>
            </plugin>

或者

mvn test -DargLine=-javaagent xxxx xxx.jar

4.如果想设置默认跳过单测,可以用maven-surefire-plugin设置skipTests=${skipTests},然后如果想进行单测,则可以直接mvn test -DskipTests=true。skipTests优先级为configuration>命令>properties中配置的。是否跳过单测最终结果为skipTests||maven.test.skip

            <properties>
                 <skipTests>true</skipTests>
            </properties>

           <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <skipTests>${skipTests}</skipTests>
                </configuration>
            </plugin>

5.maven多module项目中千万不要引入其他模块的单元测试代码: 

经过参考一些Maven的资料得知,其工作机制实际上是包的依赖管理。在规定的标准目录下,能够在模块之间引用的代码只能存在于main目录下。而单元测试(test目录下的代码)模型是建立在“独立”的思想之上的,目的就是不受其他环境的干扰从而纯粹地验证自身模块的可用性和正确性。因此单元测试代码之间是不能被其他模块引用的

6.如果使用了jacoco,它会依赖maven-surefire-plugin的argLine,所以如果你在该插件中用了argLine,建议按以下方式增加${argLine}

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <argLine>${argLine} -javaagent:xxxx xxx.jar</argLine>
                </configuration>
            </plugin>

 

参考:

https://juejin.im/post/5d62cc3ee51d45620b21c3e9

https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html

http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html

https://blog.csdn.net/chaijunkun/article/details/35796335

https://stackoverflow.com/questions/18107375/getting-skipping-jacoco-execution-due-to-missing-execution-data-file-upon-exec

 

posted @ 2019-12-19 16:20  Boblim  阅读(...)  评论(...编辑  收藏