乐趣区

关于java:一文详尽单元测试

前言

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

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

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

案例

咱们以 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

@Service
public class FooServiceImpl implements FooService {

    @Override
    public String hello() {System.out.println("foo hello");
        return "foo hello";
    }
}

测试类:FooTest

@SpringBootTest
public 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;

@SpringBootTest
public 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
@Test
public void testInsert(){fooService.insertUser(new User("李四"));
}

通常来说,不论测试任何业务都需加上 Rollback 和 Transactional 注解

Before 与 After 注解

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

@Sql(value = "/sql/test_foo_select.sql")
@BeforeEach
public void init(){// 这里能够写每个单元测试前须要做的事件}

如果你用的是 juint4, 那么应用的便是 Before 注解

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

接口测试

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

新增测试接口:

@RequestMapping("/foo")
@RestController
public 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
@SpringBootTest
public 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 申请时,代码同样如此

@Test
public 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();
}
@Service
public class BarServiceImpl implements BarService {

    @Override
    public String mock() {System.out.println("bar mock");
        return "bar mock";
    }
}

在 FooService 中增加 mock 办法

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

应用 mocktio 测试

import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.MockBean;

@SpringBootTest
public 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,当参数为其余调用实在办法

@Test
public 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 容许你自定义规定

@Test
public 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;

@SpringBootTest
public 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>

新增办法:

@Override
public 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

退出移动版