Back
Featured image of post Java单元测试利器——Mockito

Java单元测试利器——Mockito

Mock介绍

基本介绍

为了提高代码质量,除了做静态代码测试,动态的单元测试也少不了。而在单元测试过程中,Mock是少不了的技术。

Mock是允许用模拟对象替换测试中的系统部件,并断言它们是如何被使用的一项技术。

Mock的作用如下

  • 解决依赖问题:当测试一个接口或者功能模块的时候,如果这个接口或者功能模块依赖其他接口或其他模块,那么如果所依赖的接口或功能模块未开发完毕,那么我们就可以使用Mock模拟被依赖接口,完成目标接口的测试。
  • 单元测试:如果某个功能未开发完成,又要进行测试用例的代码编写,也可以先模拟这个功能进行测试。
  • 模拟复杂业务的接口:实际工作中如果我们在测试一个接口功能时,如果这个接口依赖一个非常复杂的接口业务或者来源于第三方接口(如第三方支付接口),那么我们完全可以使用Mock来模拟这个复杂的业务接口,其实这个和解决接口依赖是一样的原理。
  • 前后端联调:进行前后端分离编程时,如果进行一个前端页面开发,需要根据后合返回的状态展示不同的页面,那么就需要调用后合的接口,但是后合接口还未开发完成,完全可以借助mock来模拟后台这个接口返回想要的数据。

Mock与Stub(桩)

  • 桩代码(Stub):用来代替真实代码的临时代码,主要作用是使被测代码能够独立编译、 链接,并独立运行
  • Mock代码:也是用来代替真实代码的临时代码,起到隔离和补齐的作用,但是它还可深入的模拟对象之间的交互方式,可以对结果进行验证

Mockito测试框架

Mockito是Java单元测试中使用率最高的Mock框架之一,同时也是SpringBoot默认引入的mock框架(spring-boot-starter-test包括JUnit, Hamcrest 和 Mockito)

Maven依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>

验证某些行为是否被调用

Mockito可以验证某些行为:一旦mock对象被创建了,mock对象会记住所有的交互

使用方法verify() 判断方法是否被调用过

@Test
void verifyTest(){
    Person mockPerson = Mockito.mock(Person.class);
    mockPerson.setId(1);

    Mockito.verify(mockPerson).setId(1);
    Mockito.verify(mockPerson).setName("cc"); // 此方法没有调用过,所以会报错
}

测试桩Stub

Mockito可以做一些测试桩(Stub),做测试桩的目的是为了在某些方法还没有开发出来,但我们测试时却需要调用它的时候,给这个方法模拟一个假的返回值供我们测试用。

默认情况下,所有函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型。

当我们要指定某的函数的返回值时,可以使用when()ThenReturn()ThenThrow() 方法

之后调用这些函数时,就会返回我们上面指定的值(注意返回类型也要正确)

@Test
void stubTest(){
    Person mockPerson = Mockito.mock(Person.class);
    Mockito.when(mockPerson.getId()).thenReturn(1);
    Mockito.when(mockPerson.getName()).thenThrow(new NoSuchMethodError());

    System.out.println(mockPerson.getId());  // 返回1
    System.out.println(mockPerson.getName()); // 抛出异常NoSuchMethodError
}

一个方法可能多次调用,想要每次调用都设置对应返回值也是可以的

@Test
void constStubTest(){
    Person personMock = mock(Person.class);
    when(personMock.getName())
            .thenReturn("cc")
            .thenReturn("1");
    System.out.println(personMock.getName());  // 第一次返回cc
    System.out.println(personMock.getName());  // 第二次返回1
}

类型范围

使用anyInt()anyString()anyList()等方法可以对参数类型进行限制或者放宽范围

@Test
void matchTest(){
    Person mockPerson = Mockito.mock(Person.class);
    when(mockPerson.setKeyById(anyInt())).thenReturn("0001Test1000");
    // 表示只要传入int类型就返回"0001Test1000"
    System.out.println(mockPerson.setKeyById(10));
    verify(mockPerson).setKeyById(10);
}

方法调用次数验证

times()可以用来验证方法的调用次数,如果不是预期次数则会不通过

@Test
void timesTest(){
    Person mockPerson = mock(Person.class);
    mockPerson.setId(1);
    mockPerson.setName("cc");
    mockPerson.setName("cc");
    
    verify(mockPerson).setId(1);
    verify(mockPerson, times(2)).setName("cc");
}

方法调用顺序验证

当我们需要验证方法调用的前后顺序是否符合预期,可以InOrder 类以及它的verify() 方法

不仅使用与单个对象,多个对象的方法之间的调研顺序也可以进行判断

@Test
void orderTest() {
    Person singeMock = mock(Person.class);
    singeMock.setName("cc");
    singeMock.setName("qq");
    // 单个对象方法的调用顺序
    InOrder inOrder = inOrder(singeMock);
    inOrder.verify(singeMock).setName("cc");
    inOrder.verify(singeMock).setName("qq"); // 顺序正确
    
    Person firstMock = mock(Person.class);
    Person secondMock = mock(Person.class);
    firstMock.setId(1);
    secondMock.setId(2);
    // 多个方法的调用顺序
    InOrder inOrder1 = inOrder(firstMock, secondMock);
    inOrder1.verify(secondMock).setId(2);
    inOrder1.verify(firstMock).setId(1); // 顺序错误
}

spy监控对象

spy可以监控或控制一个真正new创建出来的对象,而不是去mock的对象,两者区别在于mock的对象如果不指定返回值就会返回null这样的零值,而spy则介于真正的bean和mock之间,对于被我们指定了返回值的优先按指定的,没有指定的就按对象实际值返回。

下面的例子是修改一个对象中某个属性的返回值,使得其返回值不在是属性的真实值

也可以用@Spy 注解

@Test
void spyTest(){
    Person person = new Person(1, "cc", null);
    Person spy = spy(person);
    when(spy.getName()).thenReturn("qq");
    System.out.println(spy.getName()); // 返回qq,而不是cc
    System.out.println(spy.getId());  // 返回1,因为没有设置过
}

Spring Boot中的单元测试

Spring Boot的结构上来说,Service层是依赖Dao层的,而Controller层又依赖于Service层

从单元测试的角度,对某个Service和Controller进行单元测试的时候,所有需要依赖的类或方法都应该mock,只针对某一段代码进行运行逻辑测试,测试Service层时,就可以使用Mockito来隔离Dao层;Dao层的单元测试可以用工具随机生成测试数据,然后使用工具测试SQL即可,就不要还启动整个应用,效率比较低。

下面是几种mock的方案

  • 在before函数中直接mock实例化对象
@BeforeEach
public void before(){
	userService = mock(UserService.class);
}
  • 使用@Mock注解来Mock对象
@Mock
private UserSerice userService;

@BeforeEach
public void before(){
	MockitoAnnotations.initMocks(this); // 此方法已被deprecad
}
  • 使用@RunWith(MockitoJunitRunner.class) ,搭配@Mock就可以直接使用了,不用再初始化(此方法只能用于Junit4)(较为推荐)
@RunWith(MockitoJunitRunner.class)
@SpringBootTest
public class Test {
	@Mock
	private UserSerice userService;

	@Test
	public void test(){
		// code
	}

}
  • 使用MockitoRule,但是对象的访问级别必须是public(此方法也只能用于Junit4)
@RunWith(JUnit4.class)
@SpringBootTest
public class Test {
  @Rule
	public MockitoRule rule = MockitoJUnit.rule();

	@Mock
	private UserSerice userService;

	@Test
	public void test(){
		// code
	}

}

单元测试常用

注解

@Test 标注一个测试方法

@BeforeEach / @AfterEach 在每个测试方法之前/后运行

@BeforeAll / @AfterAll 在整个测试流程之前/后运行,整个测试过程只会运行一次

@SpringBootTest 初始化Spring上下文来进行测试,当测试中用到类需要用Spring容器管理时加

@MockBean 同Mockito的@Mock

@SpyBean 同Mockito的@Spy

断言

  • Assert

JUnit4提供,使用较为简单,点进去看源码的方法即可。

  • Assertions

JUnit5提供,和Assert类差不多。

  • assertThat(T actual, Matcher<? super T> matcher);

assertThat(Assert类中)是JUnit 4.4引入的一个全新的断言语法,可以只使用这一个方法,实现所有的断言测试。

其中actual为需要测试的变量值,matcher为使用Hamcrest的匹配符来表达变量actual期望值的声明,如果值与matcher所表达的期望值相符,则测试成功,否则失败。

匹配器有可读性强、满足复杂匹配条件等优点,常用匹配器可以查文档,也不难使用。

comments powered by Disqus
一辈子热爱技术
Built with Hugo
Theme Stack designed by Jimmy
gopher