TDD(测试驱动开发)

Mockito

2018-07-13  本文已影响37人  jiangmo

单元测试的目标和挑战

单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。

一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。

mock测试

Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。

Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

比如一段代码有这样的依赖:



当我们需要测试A类的时候,如果没有 Mock,则我们需要把整个依赖树都构建出来,而使用 Mock 的话就可以将结构分解开,像下面这样:


Mock 对象使用范畴

真实对象具有不可确定的行为,产生不可预测的效果(如:天气预报) :

关键步骤

用Mock测试你的代码

测试驱动的开发(Test Driven Design, TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,一个很普遍的问题是,要测试的类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。

所幸,我们有一个应对这个问题的办法:Mock。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。

Mock的框架有很多,最为知名的一个是Mockito,这是一个开源项目,使用广泛。

Java Mock 测试

目前,在 Java 阵营中主要的 Mock 测试工具有 MockitoJMockEasyMock 等。

Mockito 特性

先睹为快

// 创建mock对象
List mockedList = Mockito.mock(List.class);

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");

// 使用mock对象 - 会返回前面设置好的值"one",即便列表实际上是空的
String str = mockedList.get(0);

Assert.assertTrue("one".equals(str));
Assert.assertTrue(mockedList.size() == 0);

// 验证mock对象的get方法被调用过,而且调用时传的参数是0
Mockito.verify(mockedList).get(0); 

基本分析

让我们仔细想想看,下面这个代码:

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");

如果按照一般代码的思路去理解,是要做这么一件事:

Mockito.returnValueWhen("one", mockedList, "get", 0);

第一个参数描述要返回的结果,第二个参数指定mock对象,第三个参数指定mock方法,后面的参数指定mock方法的参数值。这样的代码,更符合我们看一般代码时候的思路。

实现分析

Mock对象这件事情,本质上是一个Proxy模式的应用。

Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。Proxy对象对调用者来说,可以是透明的,也可以是不透明的。

Java本身提供了构建Proxy对象的API:Java Dynamic Proxy API。Mockito就是用Java提供的Dynamic Proxy API来实现的。

仔细分析,就会发现,示例代码最难理解的部分是:

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");

public Object handle(Invocation invocation) throws Throwable {
     if (invocationContainerImpl.hasAnswersForStubbing()) {
         ...
    }
     ...
     InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
            mockingProgress.getArgumentMatcherStorage(),
           invocation
    );
   mockingProgress.validateState();
    // if verificationMode is not null then someone is doing verify()
   if (verificationMode != null) {
   ...
      }
    // prepare invocation for stubbing   invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
  OngoingStubbingImpl<T> ongoingStubbing = 
  new OngoingStubbingImpl<T>(invocationContainerImpl);
mockingProgress.reportOngoingStubbing(ongoingStubbing);
   ...
}

注意第1行,第6-9行,可以看到方法调用的信息(invocation)对象被用来构造invocationMatcher对象,然后在第19-21行,invocationMatcher对象最终传递给了ongoingStubbing对象。完成了stub信息的保存。

小结

通过以上的分析我们可以看到,Mockito在设计时实际上有意地使用了方法的“副作用”,在返回值之外,还保存了方法调用的信息,进而在最后利用这些信息,构建出一个mock。而这些信息的保存,是对Mockito的用户完全透明的。

这是一个经典的“反模式”的使用案例。“模式”告诉我们,在设计方法的时候,应该避免副作用,一个方法在被调用时候,除了return返回值之外,不应该产生其他的状态改变,尤其不应该有“意料之外”的改变。但Mockito完全违反了这个原则,Mockito的静态方法Mockito.anyString(), mockInstance.method(), Mockito.when(), thenReturn(),这些方法,在背后都有很大的“副作用” —— 保存了调用者的信息,然后利用这些信息去完成任务。这就是为什么Mockito的代码一开始会让人觉得奇怪的原因,因为我们平时不这样写代码。

然而,作为一个Mocking框架,这个“反模式”的应用实际上是一个好的设计。就像我们前面看到的,它带来了非常简单的API,以及编译安全,可重构等优良特性。违反直觉的方法调用,在明白其原理和一段时间的熟悉之后,也显得非常的自然了。设计的原则,终究是为设计目标服务的,原则在总结出来之后,不应该成为僵硬的教条,根据需求灵活地应用这些原则,才能达成好的设计。在这方面,Mockito堪称一个经典案例。

基本用法

verify some behaviour!

 //mock creation
        List mockedList = mock(List.class);

        //using mock object
        mockedList.add("one");
        mockedList.clear();

        //verification
        verify(mockedList).add("one");
        verify(mockedList).clear();

stubbing smth

        //You can mock concrete classes, not just interfaces
        LinkedList mockedList = mock(LinkedList.class);

        //stubbing
        when(mockedList.get(0)).thenReturn("first");
        when(mockedList.get(1)).thenThrow(new RuntimeException());

        //following prints "first"
        System.out.println(mockedList.get(0));

        //following throws runtime exception
        // System.out.println(mockedList.get(1));

        //following prints "null" because get(999) was not stubbed
        System.out.println(mockedList.get(999));

        //Although it is possible to verify a stubbed invocation, usually it's just redundant
        //If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
        //If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
        verify(mockedList).get(0);

Argument matchers

Mockito verifies argument values in natural java style: by using an equals() method. Sometimes, when extra flexibility is required then you might use argument matchers:

//mock creation
        List mockedList = mock(List.class);
        //stubbing using built-in anyInt() argument matcher
        when(mockedList.get(anyInt())).thenReturn("element");

        //stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
        when(mockedList.contains(argThat(i -> (Integer)i > 10))).thenReturn(true);

        //following prints "element"
        System.out.println(mockedList.get(999));

        System.out.println(mockedList.add(30));
        System.out.println(mockedList.add(60));

        //you can also verify using an argument matcher
        verify(mockedList).get(anyInt());

        //argument matchers can also be written as Java 8 Lambdas
        verify(mockedList).add(argThat(i -> (Integer)i > 50));

Warning on argument matchers:

If you are using argument matchers, all arguments have to be provided by matchers.
The following example shows verification but the same applies to stubbing:

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher

verify(mock).someMethod(anyInt(), anyString(), "third argument");
//above is incorrect - exception will be thrown because third argument is given without an argument matcher.

Verifying exact number of invocations exact_verification/ at least x/ / never

        //mock creation
        List mockedList = mock(List.class);
        //using mock
        mockedList.add("once");

        mockedList.add("twice");
        mockedList.add("twice");

        mockedList.add("three times");
        mockedList.add("three times");
        mockedList.add("three times");

        //following two verifications work exactly the same - times(1) is used by default
        verify(mockedList).add("once");
        verify(mockedList, times(1)).add("once");

        //exact number of invocations verification
        verify(mockedList, times(2)).add("twice");
        verify(mockedList, times(3)).add("three times");

        //verification using never(). never() is an alias to times(0)
        verify(mockedList, never()).add("never happened");

        //verification using atLeast()/atMost()
        verify(mockedList, atLeastOnce()).add("three times");
        verify(mockedList, atLeast(2)).add("three times");
        verify(mockedList, atMost(5)).add("three times");

times(1) is the default. Therefore using times(1) explicitly can be omitted.

Stubbing void methods with exceptions

   doThrow(new RuntimeException()).when(mockedList).clear();

   //following throws RuntimeException:
   mockedList.clear();

Find redundant invocations

 //using mocks
 mockedList.add("one");
 mockedList.add("two");

 verify(mockedList).add("one");

 //following verification will fail
 verifyNoMoreInteractions(mockedList);

annotation mock

更多官方详细使用示例参考文末Ref 使用

静态引用

如果在代码中静态引用了org.mockito.Mockito.*;那就可以直接调用静态方法和静态变量而不用创建对象,譬如直接调用 mock() 方法。
这个对于Mockito很好用,单是一般我们会配置包的明确引用,不是*。

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

如果你使用注解,则必须要实例化 mock 对象。

Mockito 在遇到使用注解的字段的时候,会调用MockitoAnnotations.initMocks(this) 来初始化该 mock 对象。另外也可以通过使用@RunWith(MockitoJUnitRunner.class)来达到相同的效果。

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)
        }
}
// =================说明
- 1. 告诉 Mockito 模拟 databaseMock 实例

- 2. Mockito 通过 @mock 注解创建 mock 对象

- 3. 使用已经创建的mock初始化这个类

- 4. 在测试环境下,执行测试类中的代码

- 5. 使用断言确保调用的方法返回值为 true

- 6. 验证 query 方法是否被 MyDatabase 的 mock 对象调用

配置 mock

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

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

@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 方法
@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();
}

验证 mock 对象方法是否被调用(即所谓的行为验证)

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

@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));

        // 方法是否被调用两次,默认是1次
        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");
}

使用 Spy 封装 java 对象

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

// 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");

@InjectMocks 在 Mockito 中进行依赖注入

// 假定我们有 ArticleManager 类
public class ArticleManager {
    private User user;
    private ArticleDatabase database;
    ArticleManager(User user) {
     this.user = user;
    }
    void setDatabase(ArticleDatabase database) { }
}


@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest  {

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

       @Spy private UserProvider userProvider = new ConsumerUserProvider();
        // 这个类会被 Mockito 构造,而类的成员方法和变量都会被 mock 对象所代替
       @InjectMocks private ArticleManager manager;// (1)

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

               // 验证 addListener 方法被调用
               verify(database).addListener(any(ArticleListener.class));
       }
}

捕捉参数Captor

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

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"));
    }
}

Mockito的限制

而下面三种数据类型则不能够被测试

实例:使用 Mockito 创建一个 mock 对象

// 创建一个Twitter API 的例子
public interface ITweet {
        String getMessage();
}

public class TwitterClient {
        public void sendTweet(ITweet tweet) {
                String message = tweet.getMessage();
                // send the message to Twitter
        }
}

// 模拟 ITweet 的实例
@Test
public void testSendingTweet() {
        TwitterClient twitterClient = new TwitterClient();

        ITweet iTweet = mock(ITweet.class);

        when(iTweet.getMessage()).thenReturn("Using mockito is great");

        twitterClient.sendTweet(iTweet);
}

// 验证方法调用
@Test
public void testSendingTweet() {
        TwitterClient twitterClient = new TwitterClient();

        ITweet iTweet = mock(ITweet.class);

        when(iTweet.getMessage()).thenReturn("Using mockito is great");

        twitterClient.sendTweet(iTweet);
        // 验证 getMessage() 方法至少调用一次。
        verify(iTweet, atLeastOnce()).getMessage();
}

模拟静态方法

因为 Mockito 不能够 mock 静态方法,因此我们可以使用 Powermock。

// 模拟了 NetworkReader 的依赖
@RunWith( PowerMockRunner.class )
@PrepareForTest( NetworkReader.class )
public class MyTest {

final class NetworkReader {
    public static String getLocalHostname() {
        String hostname = "";
        try {
            InetAddress addr = InetAddress.getLocalHost();
            // Get hostname
            hostname = addr.getHostName();
        } catch ( UnknownHostException e ) {
        }
        return hostname;
    }
}

// 测试代码

 @Test
public void testSomething() {
    mockStatic( NetworkUtil.class );
    when( NetworkReader.getLocalHostname() ).andReturn( "localhost" );
}

// 有时候我们可以在静态方法周围包含非静态的方法来达到和 Powermock 同样的效果。
class FooWraper { 
      void someMethod() { 
           Foo.someStaticMethod() 
       } 
}

Ref:
官网:http://site.mockito.org/
介绍:http://www.infoq.com/cn/articles/mockito-design/
使用:
https://juejin.im/entry/578f11aec4c971005e0caf82
http://static.javadoc.io/org.mockito/mockito-core/2.19.0/org/mockito/Mockito.html
git:https://github.com/mockito/mockito
其他:
https://waylau.com/mockito-quick-start/

上一篇 下一篇

猜你喜欢

热点阅读