前言

如果你认为单元测试会升高开发效率,那么它做的事就是让你的开发效率少升高一点;如果你认为单元测试能够进步开发效率,那么祝贺你,它会是一份宝藏。

这是一篇涵盖了大部分场景下须要用到的单元测试办法介绍,不论你是老手还是老鸟,都倡议读读看。

本文并不会去传导单元测试的重要性之类的思维,这不是本文的重点,本文只阐明如何写单元测试

案例

咱们以SpringBoot构建一个简略的demo

引入依赖:

<!-- web环境,为前面的接口测试所筹备--><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId></dependency><!-- 测试包 --><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-test</artifactId>  <scope>test</scope></dependency>

命名标准

测试类的命名个别要以Test为后缀,例如:XxxTest

测试方法的命名个别要以test为前缀,例如:testXxx

留神:如果你的类名不是XxxTest,那么你在执行相似maven test命令时,是不会自动测试这个类的。

这个标准是在maven-surefire-plugin插件中约定的,你也能够自定义的设置你本人的命名标准,然而不倡议这样做。

简略测试

简略测试只需在测试方法上加上Test注解即可

实用场景:测试一些工具类,验证心中所想(比方忘了正则怎么写了)

新建测试类: HelloTest, 测试方法:testHello

import org.junit.jupiter.api.Test;public class HelloTest {        @Test    public void testHello(){        System.out.println("Hello World!");    }}

接下来只需微微点击测试按钮

1、运行整个测试类,测试类中所有的测试方法(加了Test注解的)

2、运行这个测试方法(点开运行形式的界面)

3、间接运行这个测试方法

4、以debug的形式运行这个测试方法

5、以测试覆盖率的形式运行这个测试方法

个别是点3、4这两个

成果:

对于断言

断言的意思就是:... 断言!

有时候咱们测试了某个办法,在过后咱们晓得后果是正确的,然而很可能过了几天:咦,这代码是我写的?

所以加个断言就很有必要了,它能让咱们晓得:只有测试后果通过了断言,那么就是这个被测试的办法就是正确的。如果没有通过,那就须要好好检查一下代码了!

那么断言应该怎么写呢?

import org.hamcrest.CoreMatchers;import org.hamcrest.MatcherAssert;import org.junit.jupiter.api.Test;public class HelloTest {    @Test    public void testAssert(){        int a = 1, b =2 ;        // 断言        MatcherAssert.assertThat(a + b, CoreMatchers.is(3));    }}

第一个参数是理论测试的后果,第二个是match函数,外面放的是期望值

你也能够用junit的Assert办法,我比拟喜爱下面的

业务测试

所谓业务测试就是测试你的业务代码,这种状况下,咱们就须要用Spring环境了。

新建接口: FooService

public interface FooService {    String hello();}

实现类:FooServiceImpl

@Servicepublic class FooServiceImpl implements FooService {    @Override    public String hello() {        System.out.println("foo hello");        return "foo hello";    }}

测试类:FooTest

@SpringBootTestpublic class FooServiceTest {    @Autowired    private FooService fooService;    @Test    public void testHello(){        String hello = fooService.hello();        MatcherAssert.assertThat(hello, CoreMatchers.is("foo hello"));    }}

留神:如果你的Test注解是junit4的: org.junit.Test,那么还须要在类上再加一个注解:@RunWith(SpringRunner.class)

数据测试

基本上每一个业务代码都离不开数据库,那么在做数据测试时,就离不开两个问题:

1、初始数据从哪里来(比方在做查问测试时)

2、测试产生的数据如何革除(比方在做新增测试时)

问题1:咱们能够在测试方法上减少@Sql注解用于初始化数据

问题2:咱们能够在测试方法上减少@Transactional@Rollback注解用于测试结束主动回滚

案例:

假如咱们要测试查问逻辑,首先咱们在test/resourcs下新建sql目录,用于寄存初始化数据sql

接着在sql目录中新建test_foo_select.sql文件

insert into user (`name`) values ('张三');

新建测试方法:

import org.springframework.test.annotation.Rollback;import org.springframework.test.context.jdbc.Sql;import org.springframework.transaction.annotation.Transactional;@SpringBootTestpublic class FooServiceTest {    @Autowired    private FooService fooService;    @Transactional    @Rollback    @Sql(value = "/sql/test_foo_select.sql")    @Test    public void testSelect(){        // 假如该办法中调用了数据库        User user = fooService.selectUser("张三");        MatcherAssert.assertThat(user.getName(), CoreMatchers.is("张三"));    }}
@Transactional和@Rollback注解是为了回滚初始化的测试数据

假如要测试批改数据逻辑

@Rollback@Transactional@Testpublic void testInsert(){  fooService.insertUser(new User("李四"));}
通常来说,不论测试任何业务都需加上Rollback和Transactional注解

Before与After注解

如果在你的单元测试类中,所有办法都依赖于一份初始化数据文件,那么你还能够这样写

@Sql(value = "/sql/test_foo_select.sql")@BeforeEachpublic void init(){    // 这里能够写每个单元测试前须要做的事件}
如果你用的是juint4, 那么应用的便是Before注解

同样,还有AfterEachAfter注解,应用形式雷同,这里就不再赘述。

接口测试

以上测试是在测试业务层逻辑,有时候咱们还须要测试接口层逻辑,比如说参数校验

新增测试接口:

@RequestMapping("/foo")@RestControllerpublic class FooController {    @GetMapping    public User getUser(String name){        return new User(name);    }}

新增测试类:

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.mock.web.MockHttpServletResponse;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.ResultActions;import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import org.springframework.test.web.servlet.result.MockMvcResultMatchers;@AutoConfigureMockMvc@SpringBootTestpublic class FooControllerTest {    @Autowired    private MockMvc mockMvc;    @Test    public void testGet() throws Exception {        // 构建申请        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/foo?name=张三");        // 发动申请        ResultActions resultActions = mockMvc.perform(builder);        // 获取后果        MockHttpServletResponse response = resultActions.andReturn().getResponse();        response.setCharacterEncoding("UTF-8");        // 断言http响应状态码是否为2xx        resultActions.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());        // 获取响应数据        String result = response.getContentAsString();        User user = JSON.parseObject(result, User.class);        MatcherAssert.assertThat(user.getName(), CoreMatchers.is("张三"));    }}

测试接口尽管看起来很简单,然而外面大多是样板代码,在理论开发中,能够将这些样板代码封装到工具中

比方测试post申请时,代码同样如此

@Testpublic void testPost() throws Exception {  // 构建申请, 这里是惟一的变动,将get改为了post  MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/foo");  // 发动申请  ResultActions resultActions = mockMvc.perform(builder);  // 获取后果  MockHttpServletResponse response = resultActions.andReturn().getResponse();  response.setCharacterEncoding("UTF-8");  // 断言http响应状态码是否为2xx  resultActions.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());  // 获取响应数据  String result = response.getContentAsString();  MatcherAssert.assertThat(result, CoreMatchers.is("true"));}

MockMvcRequestBuilders外面有很多办法,这里给出罕用的几个

// post申请MockMvcRequestBuilders.post("/foo")                            // 申请参数                .queryParam("key", "value")                // header                .header("token", "123456")                .accept(MediaType.APPLICATION_JSON)                .contentType(MediaType.APPLICATION_JSON)                // 申请body                .content(JSON.toJSONString(new User("张三")))

Mock测试

在现在分布式、微服务越来越火的状况下,一个零碎总是不可避免的会与其余零碎交互,然而在测试时,咱们是不心愿产生这种状况的,因为这样就须要依赖外部环境了。

单元测试的准则便是:可能独立运行。

此时,学会mock测试就是一件十分有必要的事件。

对于Mockito

spring-boot-test中,自带一个叫Mockito的工具,它可能帮忙咱们对不想调用的办法进行拦挡,并且返回咱们冀望的后果

比方有一个FooService调用BarService的场景

当咱们在测试时不想要真正调用barService,那么咱们就能够应用Mockito进行拦挡

根本Mock

新增BarService

public interface BarService {    String mock();}
@Servicepublic class BarServiceImpl implements BarService {    @Override    public String mock() {        System.out.println("bar mock");        return "bar mock";    }}

在FooService中增加mock办法

@Overridepublic String mock() {  System.out.println("foo mock");  return barService.mock();}

应用mocktio测试

import org.mockito.Mockito;import org.springframework.boot.test.mock.mockito.MockBean;@SpringBootTestpublic class FooServiceTest {    @Autowired    private FooService fooService;    // 应用MockBean注解注入barService    @MockBean    private BarService barService;      @Test    public void testMock(){        // 当调用barService.mock办法是返回it's mock        Mockito.doReturn("it's mock").when(barService).mock();        String mock = fooService.mock();        MatcherAssert.assertThat(mock, CoreMatchers.is("it's mock"));    }}

应用参数管制mock

可能有时候会有这种奇怪的需要,当参数为1时应用mock,当参数为其余调用实在办法

@Testpublic void testMockHasParam(){  // 当参数为1时失效  Mockito.doReturn("it's mock").when(barService).mock(Mockito.eq(1));  String mock = fooService.mock(1);  MatcherAssert.assertThat(mock, CoreMatchers.is("it's mock"));}
如果你感觉任何参数都应该应用mock,那你能够在参数上写:Mockito.any()

Mockito中还有很多相似的办法,如果你感觉还不满足,mockito容许你自定义规定

@Testpublic void testMockHasParam2() {  // 当参数为1时失效  Mockito.doReturn("it's mock")    .when(barService)    .mock(Mockito.intThat(arg -> {      // 这里写你的逻辑      return arg.equals(1);    }));  String mock = fooService.mock(1);  MatcherAssert.assertThat(mock, CoreMatchers.is("it's mock"));}

留神:尽管以上案例看起来像:当参数不为1时就调用实在办法,但实际上并不是的,因为barService实际上是Mockito生成的代理类,仅仅是个代理类,它并未持有真正的barService, 所以当不满足mock逻辑时,它永远都是返回null

那么该如何解决这个问题呢?

局部办法Mock

参数管制mock与局部办法mock的场景是共通的:在特定的状况下须要调用实在办法

改变形式特地简略:将原来的@MockBean注解替换为@SpyBean

SpyBean注解是真正的将Spring容器中的BarService进行代理,而不是简略的仅仅生成代理类,所以它具备了真正调用办法的能力

比方咱们在FooService中新增办法:partMock

public String partMock() {  barService.hello();  return barService.mock();}

当初咱们冀望在barService.hello()调用理论办法,调用barService.mock()时被mockito拦挡

import org.springframework.boot.test.mock.mockito.SpyBean;@SpringBootTestpublic class FooServiceTest {    @Autowired    private FooService fooService;    @SpyBean    private BarService barService;    @Test    public void testPartMock() {        Mockito.doReturn("it's mock").when(barService).mock();        final String mock = fooService.partMock();        MatcherAssert.assertThat(mock, CoreMatchers.is("it's mock"));    }}
你会发现应用形式没有任何的变动

静态方法Mock

你可能想问,为什么Mock还要辨别是不是静态方法?这是因为静态方法mock是Mockito所不具备的能力,咱们须要另外一个组件来实现:powermock

但很惋惜的是,powermock只反对junit4,而且最近的release是在2020年11月2日

不管怎样,咱们还是应该学习它,让咱们在将来可能遇到这种问题时有解决办法

引入依赖:

<dependency>  <groupId>org.powermock</groupId>  <artifactId>powermock-module-junit4</artifactId>  <version>2.0.2</version>  <scope>test</scope></dependency><dependency>  <groupId>org.powermock</groupId>  <artifactId>powermock-api-mockito2</artifactId>  <version>2.0.2</version>  <scope>test</scope></dependency>

新增办法:

@Overridepublic String powermock() {  return JSON.toJSONString(new User("张三"));}

当初,咱们想要拦挡JSON.toJSONString办法,并且冀望它返回xxx

import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.Mockito;import org.powermock.api.mockito.PowerMockito;import org.powermock.core.classloader.annotations.PowerMockIgnore;import org.powermock.core.classloader.annotations.PrepareForTest;import org.powermock.modules.junit4.PowerMockRunner;import org.powermock.modules.junit4.PowerMockRunnerDelegate;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;// 代理SpringRunner@PowerMockRunnerDelegate(SpringRunner.class)// 应用PowerMockRunner@RunWith(PowerMockRunner.class)// 提早加载以下包中的所有类@PowerMockIgnore(value = { "javax.management.*", "javax.net.ssl.*", "javax.net.SocketFactory", "oracle.*"})@SpringBootTest// 想要mock的类@PrepareForTest(JSON.class)public class PowermockTest {    @Autowired    private FooService fooService;    @Test    public void testPowermock(){        // 固定写法        PowerMockito.mockStatic(JSON.class);        // 以下写法与mockito雷同        Mockito.when(JSON.toJSONString(Mockito.any())).thenReturn("xxx");        String s = fooService.powermock();        MatcherAssert.assertThat(s, CoreMatchers.is("xxx"));    }}

对于PowerMockIgnore注解笔者也不是太懂其中的原理,如果你在测试时发现哪个包报错,并且是你看不懂的,那么你就把这个包加到这外面就好了。

配置文件的划分

大部分状况下,单元测试时所应用的配置与理论在服务器上运行时所用的配置是雷同的,那么咱们就能够独自在test/resources包下放入测试所用配置。

留神:测试包下的配置文件与main/resources下的配置文件是替换的关系

比方测试包下有一个application.yaml文件,外面的配置为:

abc: xxx

main/resources下也有一个application.yaml文件,外面的配置为:

def: xxx

理论运行时并非像平常一样是合并所有配置,而是只存在

abc: xxx

利用这样的办法,咱们能够在单元测试时指定咱们须要的环境,比方在微服务零碎中单元测试时不须要连贯注册核心,那么咱们就能够在配置文件中将它关掉。

小结

编写单元测试是一件结尾较难的事,对于未接触过单元测试的开发人员来说,可能编写一个接口须要1个小时,然而在编写单元测试的功夫上须要破费2个小时。本文的目标就在于可能让这样的同学疾速的学习编写单元测试,让写单元测试也能高兴起来。

心愿小伙伴们最终都能达到:单元测试能够进步开发效率

案例地址:https://gitee.com/lzj960515/j...


如果我的文章对你有所帮忙,还请帮忙点赞、关注、转发一下,你的反对就是我更新的能源,非常感谢!

集体博客空间:https://zijiancode.cn