Mock 及 Mockito 教程

Mockito 教程

Mockito框架官方地址mockito,文档地址,中文版文档。

Mockito库能够Mock对象、验证结果以及打桩(stubbing)。

1. Mock和Mockito的关系

在软件开发中提及mock,通常理解为模拟对象。

为什么需要模拟? 在我们一开始学编程时,我们所写的对象通常都是独立的,并不依赖其他的类,也不会操作别的类。但实际上,软件中是充满依赖关系的,比如我们会基于service类写操作类,而service类又是基于数据访问类(DAO)的,依次下去,形成复杂的依赖关系。

单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。

有些时候,我们代码所需要的依赖可能尚未开发完成,甚至还不存在,那如何让我们的开发进行下去呢?使用mock可以让开发进行下去,mock技术的目的和作用就是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。

我们可以自己编写自定义的Mock对象实现mock技术,但是编写自定义的Mock对象需要额外的编码工作,同时也可能引入错误。现在实现mock技术的优秀开源框架有很多,Mockito就是一个优秀的用于单元测试的mock框架。Mockito已经在github上开源,详细请点击:https://github.com/mockito/mockito

除了Mockito以外,还有一些类似的框架,比如:

EasyMock:早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常

PowerMock:这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了

JMockit:JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode

Mockito已经被广泛应用,所以这里重点介绍Mockito。

2. 使用mock对象来进行测试

2.1. 单元测试的目标和挑战

单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。

2.2. 测试类的分类

dummy object 做为参数传递给方法但是绝对不会被使用。譬如说,这种测试类内部的方法不会被调用,或者是用来填充某个方法的参数。

Fake 是真正接口或抽象类的实现体,但给对象内部实现很简单。譬如说,它存在内存中而不是真正的数据库中。(译者注:Fake 实现了真正的逻辑,但它的存在只是为了测试,而不适合于用在产品中。)

stub 类是依赖类的部分方法实现,而这些方法在你测试类和接口的时候会被用到,也就是说 stub 类在测试中会被实例化。stub 类会回应任何外部测试的调用。stub 类有时候还会记录调用的一些信息。

mock object 是指类或者接口的模拟实现,你可以自定义这个对象中某个方法的输出结果。

测试替代技术能够在测试中模拟测试类以外对象。因此你可以验证测试类是否响应正常。譬如说,你可以验证在 Mock 对象的某一个方法是否被调用。这可以确保隔离了外部依赖的干扰只测试测试类。

我们选择 Mock 对象的原因是因为 Mock 对象只需要少量代码的配置。

2.3. Mock 对象的产生

你可以手动创建一个 Mock 对象或者使用 Mock 框架来模拟这些类,Mock 框架允许你在运行时创建 Mock 对象并且定义它的行为。

一个典型的例子是把 Mock 对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是我们在测试的时候 Mock 对象将会模拟成数据提供者来确保我们的测试环境始终是相同的。

Mock 对象可以被提供来进行测试。因此,我们测试的类应该避免任何外部数据的强依赖。

通过 Mock 对象或者 Mock 框架,我们可以测试代码中期望的行为。譬如说,验证只有某个存在 Mock 对象的方法是否被调用了。

2.4. 使用 Mockito 生成 Mock 对象

Mockito 是一个流行 mock 框架,可以和JUnit结合起来使用。Mockito 允许你创建和配置 mock 对象。使用Mockito可以明显的简化对外部依赖的测试类的开发。

一般使用 Mockito 需要执行下面三步

  • 模拟并替换测试代码中外部依赖。
  • 执行测试代码
  • 验证测试代码是否被正确的执行

默认情况下,对于所有有返回值且没有stub过的方法,mockito会返回相应的默认值。

对于内置类型会返回默认值,如int会返回0,布尔值返回false。对于其他type会返回null。

这里一个重要概念就是: mock对象会覆盖整个被mock的对象,因此没有stub的方法只能返回默认值。

3. Mockito 注意事项

3.1 mockito之@Mock和@InjectMock

Mockito是java单元测试中,最常用的mock工具之一,提供了诸多打桩方法和注解。其中有两个比较常用的注解,@Mock和@InjectMock,名字和在代码中使用 的位置都很像,对于初学者,很容易误解。下面花一点时间,做个简单的介绍。

介绍之前,首先要明确一点:@Mock和@InjectMock这两个注解除了名字和使用方式比较像之外,是在功能上无任何可类比性的完全不同的东西。

@Mock:

在Mockito中用于创建mock对象,使用方法如下:

@Mock
private ClassName mockedObject;

上面代码创建了一个名为mockedObject,类型为ClassName的mock对象,该对象所有的方法被置空,根据测试代码逻辑的需要使用

@InjectMock:

这是一个注入mock对象的操作,参考如下代码:

@Mock
private ClassName mockedObject;

@InjectMock
private TestedClass TestedObj = new TestedClass();

这段代码中,@InjectMock下面声明了一个待测试的对象,若该对象有类型为ClassName的成员变量,@Mock定义的mock对象将会被注入到这个待测试的对象中,即TestedObj的类型为ClassName的成员被直接赋值为mockedObject。(熟悉依赖注入的同学应该很容易理解)

补充几点:

  1. @Mock创建的是全部mock的对象,即在对具体的方法打桩之前,mock对象的所有属性和方法全被置空(0或者null);与之对应的是@Spy这个注解,@Spy可以创建部分mock的对象,部分mock对象的所有成员方法都会按照原方法的逻辑执行,直到被打桩返回某个具体的值。@Mock和@Spy才是两个具有可比性的概念。

  2. Mokcito的mock()方法功能与@Mock相同,只是使用方式和场景不同。同样的,@Spy也对应一个spy()方法。

  3. @Mock和@Spy注解的对象,均可被@InjectMock注入到待处理的对象中。

3.2 Mockito when(…).thenReturn(…)和doReturn(…).when(…)的区别

在Mockito中打桩(即stub)有两种方法when(…).thenReturn(…)和doReturn(…).when(…)。这两个方法在大部分情况下都是可以相互替换的,但是在使用了Spies对象(@Spy注解),而不是mock对象(@Mock注解)的情况下他们调用的结果是不相同的(目前我只知道这一种情况,可能还有别的情形下是不能相互替换的)。
● when(…).thenReturn(…)会调用真实的方法,如果你不想调用真实的方法而是想要mock的话,就不要使用这个方法。
● doReturn(…).when(…) 不会调用真实方法

@Test
void getUserInfoByIdTest() {
    final SysUserInfo userInfo = SysUserInfo.builder()
            .id(1L)
            .userName("admin")
            .password("123456")
            .sex(2)
            .age(99)
            .email("[email protected]")
            .createUser("admin")
            .createTime(LocalDateTime.now())
            .updateUser("admin")
            .updateTime(LocalDateTime.now())
            .build();
    Mockito.when(userInfoMapper.selectById(any())).thenReturn(userInfo);
    // 或者
    // BDDMockito.given(userInfoMapper.selectById(any())).willReturn(userInfo);
    final SysUserInfo info = userInfoService.getById(1);
    Assertions.assertNotNull(info);
}

当以下情况时,使用带do的方法

  • 存根void方法
  • 在spy对象上存根方法
  • 多次存根相同的方法,以在测试过程中更改模拟的行为

常用的带do的方法有doThrow()、doAnswer()、doNothing()、doReturn()和doCallRealMethod()

  • doReturn():用于当监视真实对象并在spy上调用真实方法时会带来副作用,或覆盖先前的异常存根
  • doThrow():用于给void方法存根需要有异常抛出
  • doAnswer():用于给void方法存根需要有回调值
  • doNothing():用于给void方法存根不需要做任何事,使用情况为对void方法连续调用进行存根或者监视真实对象且void方法不执行任何操作
  • doCallRealMethod():用于调用真正执行的方法

4. 使用Mockito API

4.1. 静态引用

如果在代码中静态引用了org.mockito.Mockito.*;,那你你就可以直接调用静态方法和静态变量而不用创建对象,譬如直接调用 mock() 方法。

4.2. 使用 Mockito 创建和配置 mock 对象

除了上面所说的使用 mock() 静态方法外,Mockito 还支持通过 @Mock 注解的方式来创建 mock 对象。

如果你使用注解,那么必须要实例化 mock 对象。Mockito 在遇到使用注解的字段的时候,有三种方式来初始化该mock对象。

  • MockitoAnnotations.initMocks(this)
  • @RunWith(MockitoJUnitRunner.class)
  • @RuleMockitoJUnit.rule()

@RunWith(MockitoJUnitRunner.class)方式

@RunWith(MockitoJUnitRunner.class)
public class MockitoJUnitRunnerTest{

  @Mock
  private UserDao userDao;
  
}

或者

@RunWith(MockitoJUnitRunner.Silent.class)
public class MockitoJUnitRunnerTest{

  @Mock
  private UserDao userDao;
  
}

MockitoAnnotations.initMocks(this)方式

public class MockitoAnnotationsTest{
	
  @Mock
  private UserDao userDao;
  
  @Before
  public void setUp(){
    MockitoAnnotations.initMocks(this);
  }
}

@RuleMockitoJUnit.rule()

import static org.mockito.Mockito.*;

public class MockitoTest  {

  @Mock
  MyDatabase databaseMock; //(1)

  @Rule 
  public MockitoRule mockitoRule = MockitoJUnit.rule(); //(2)

  @Test
  public void testQuery()  {
    ClassToTest t  = new ClassToTest(databaseMock); //(3)
    boolean check = t.query("* from t"); //(4)
    assertTrue(check); //(5)
    verify(databaseMock).query("* from t"); //(6)
   }
}

或者把上面的第(2)步设置成如下模式

	@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
  1. 告诉 Mockito 模拟 databaseMock 实例
  2. Mockito 通过 @mock 注解创建 mock 对象
  3. 使用已经创建的mock初始化这个类
  4. 在测试环境下,执行测试类中的代码
  5. 使用断言确保调用的方法返回值为 true
  6. 验证 query 方法是否被 MyDatabase 的 mock 对象调用

其中MockitoJUnitRunner有三种方式

  • Silent:实现忽略存根参数不匹配 (MockitoJUnitRunner.StrictStubs) 并且不检测未使用的存根
  • Strict:检测未使用的存根并将它们报告为失败
  • StrictStubs:改进调试测试,帮助保持测试干净

MockitoRule有两种方式

  • silent():不会在测试执行期间报告存根警告
  • strictness(Strictness):严格级别,尤其是“严格存根”(Strictness.STRICT_STUBS)有助于调试和保持测试清洁,另外还有严格级别LENIENT和WARN

4.3. 配置 mock

当我们需要配置某个方法的返回值的时候,Mockito 提供了链式的 API 供我们方便的调用

when(….).thenReturn(….)可以被用来定义当条件满足时函数的返回值,如果你需要定义多个返回值,可以多次定义。当你多次调用函数的时候,Mockito 会根据你定义的先后顺序来返回返回值。Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说你的函数可以将anyString 或者 anyInt作为输入参数,然后定义其特定的放回值。

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@Test
public void test1()  {
        //  创建 mock
        MyClass test = Mockito.mock(MyClass.class);

        // 自定义 getUniqueId() 的返回值
        when(test.getUniqueId()).thenReturn(43);

        // 在测试中使用mock对象
        assertEquals(test.getUniqueId(), 43);
}

// 返回多个值
@Test
public void testMoreThanOneReturnValue()  {
        Iterator i= mock(Iterator.class);
        when(i.next()).thenReturn("Mockito").thenReturn("rocks");
        String result=i.next()+" "+i.next();
        // 断言
        assertEquals("Mockito rocks", result);
}

// 如何根据输入来返回值
@Test
public void testReturnValueDependentOnMethodParameter()  {
        Comparable c= mock(Comparable.class);
        when(c.compareTo("Mockito")).thenReturn(1);
        when(c.compareTo("Eclipse")).thenReturn(2);
        // 断言
        assertEquals(1,c.compareTo("Mockito"));
}

// 如何让返回值不依赖于输入
@Test
public void testReturnValueInDependentOnMethodParameter()  {
        Comparable c= mock(Comparable.class);
        when(c.compareTo(anyInt())).thenReturn(-1);
        // 断言
        assertEquals(-1 ,c.compareTo(9));
}

// 根据参数类型来返回值
@Test
public void testReturnValueInDependentOnMethodParameter()  {
        Comparable c= mock(Comparable.class);
        when(c.compareTo(isA(Todo.class))).thenReturn(0);
        // 断言
        Todo todo = new Todo(5);
        assertEquals(todo ,c.compareTo(new Todo(1)));
}

对于无返回值的函数,我们可以使用doReturn(…).when(…).methodCall来获得类似的效果。例如我们想在调用某些无返回值函数的时候抛出异常,那么可以使用doThrow 方法。如下面代码片段所示

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

// 下面测试用例描述了如何使用doThrow()方法

@Test(expected=IOException.class)
public void testForIOException() {
        // 创建并配置 mock 对象
        OutputStream mockStream = mock(OutputStream.class);
        doThrow(new IOException()).when(mockStream).close();

        // 使用 mock
        OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
        streamWriter.close();
}

4.4. 验证 mock 对象方法是否被调用

Mockito 会跟踪 mock 对象里面所有的方法和变量。所以我们可以用来验证函数在传入特定参数的时候是否被调用。这种方式的测试称行为测试,行为测试并不会检查函数的返回值,而是检查在传入正确参数时候函数是否被调用。

import static org.mockito.Mockito.*;

@Test
public void testVerify()  {
        // 创建并配置 mock 对象
        MyClass test = Mockito.mock(MyClass.class);
        when(test.getUniqueId()).thenReturn(43);

        // 调用mock对象里面的方法并传入参数为12
        test.testing(12);
        test.getUniqueId();
        test.getUniqueId();

        // 查看在传入参数为12的时候方法是否被调用
        verify(test).testing(Matchers.eq(12));

        // 方法是否被调用两次
        verify(test, times(2)).getUniqueId();

        // 其他用来验证函数是否被调用的方法
        verify(mock, never()).someMethod("never called");
        verify(mock, atLeastOnce()).someMethod("called at least once");
        verify(mock, atLeast(2)).someMethod("called at least twice");
        verify(mock, times(5)).someMethod("called five times");
        verify(mock, atMost(3)).someMethod("called at most 3 times");
}

4.5. 使用 Spy 封装 java 对象

@Spy或者spy()方法可以被用来封装 java 对象。被封装后,除非特殊声明(打桩 stub),否则都会真正的调用对象里面的每一个方法

import static org.mockito.Mockito.*;

// Lets mock a LinkedList
List list = new LinkedList();
List spy = spy(list);

// 可用 doReturn() 来打桩
doReturn("foo").when(spy).get(0);

// 下面代码不生效
// 真正的方法会被调用
// 将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空
when(spy.get(0)).thenReturn("foo");

方法verifyNoMoreInteractions()允许你检查没有其他的方法被调用了。

4.6. 使用 @InjectMocks 在 Mockito 中进行依赖注入

我们也可以使用@InjectMocks 注解来创建对象,它会根据类型来注入对象里面的成员方法和变量。简单的说就是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。可以调用类中的真实方法。假定我们有 ArticleManager 类

public class ArticleManager {
    private User user;
    private ArticleDatabase database;

    ArticleManager(User user) {
     this.user = user;
    }

    void setDatabase(ArticleDatabase database) { }
}

这个类会被 Mockito 构造,而类的成员方法和变量都会被 mock 对象所代替,正如下面的代码片段所示:

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest  {

       @Mock ArticleCalculator calculator;
       @Mock ArticleDatabase database;
       @Most User user;

       @Spy private UserProvider userProvider = new ConsumerUserProvider();

       @InjectMocks private ArticleManager manager; (1)

       @Test public void shouldDoSomething() {
               // 假定 ArticleManager 有一个叫 initialize() 的方法被调用了
               // 使用 ArticleListener 来调用 addListener 方法
               manager.initialize();

               // 验证 addListener 方法被调用
               verify(database).addListener(any(ArticleListener.class));
       }
}
  1. 创建ArticleManager实例并注入Mock对象

更多的详情可以查看
http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html.

4.7. 捕捉参数

ArgumentCaptor类允许我们在verification期间访问方法的参数。得到方法的参数后我们可以使用它进行测试。

import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.util.Arrays;
import java.util.List;

import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

public class MockitoTests {

        @Rule public MockitoRule rule = MockitoJUnit.rule();

        @Captor
    private ArgumentCaptor> captor;

        @Test
    public final void shouldContainCertainListItem() {
        List asList = Arrays.asList("someElement_test", "someElement");
        final List mockedList = mock(List.class);
        mockedList.addAll(asList);

        verify(mockedList).addAll(captor.capture());
        final List capturedArgument = captor.>getValue();
        assertThat(capturedArgument, hasItem("someElement"));
    }
}

4.8. Mockito的限制

Mockito当然也有一定的限制。而下面三种数据类型则不能够被测试

  • final classes
  • anonymous classes
  • primitive types

你可能感兴趣的:(Java,java,单元测试,开发语言)