Mockito的使用

本篇文章将对Mockito重要的API进行梳理.

另外, GItHub上有相应的翻译好的中文文档: https://github.com/hehonghui/mockito-doc-zh/blob/master/README.md#0

搭建Mockito测试环境

前些文章已有过描述,重温一下.

dependencies {
    // ... more entries
    testCompile 'junit:junit:4.12'

    // required if you want to use Mockito for unit tests
    testCompile 'org.mockito:mockito-core:2.7.22'
    // required if you want to use Mockito for Android tests
    androidTestCompile 'org.mockito:mockito-android:2.7.22'
}

使用Mockito创建mock对象

Mockito提供几种创建mock对象的方法:

  • 使用静态方法 mock()
  • 使用注解 @Mock 标注

如果使用@Mock注解, 必须去触发所标注对象的创建. 可以使用 MockitoRule来实现. 它调用了静态方法MockitoAnnotations.initMocks(this) 去初始化这个被注解标注的字段.或者也可以使用@RunWith(MockitoJUnitRunner.class).

JUnit Rule请回顾之前文章

具体的用法可以参照下面示例:

import static org.mockito.Mockito.*;
public class ClassToTestTest {
    @Mock
    MyDatabase databaseMock;//①
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();//②
    @Test
    public void query() throws Exception {
        ClassToTest t  = new ClassToTest(databaseMock);//③
        boolean check = t.query("* from t");//④
        assertTrue(check);//⑤
        verify(databaseMock).query("* from t");//⑥
    }
}
  1. 通知Mockito模拟databaseMock实例
  2. 通知Mockito创建被 @Mock 注解标注 的模拟对象(本例中就是databaseMock)
  3. 用上一步创建的模拟对象去实例化测试类
  4. 执行测试代码
  5. 断言返回值为true
  6. 验证MyDatabase中的query方法被调用

再次重申一下静态导入的重要性,使用静态导入可以非常好的提高代码的可读性,你值得拥有

配置模拟对象

Mockito可以通过自然的API来实现模拟对象的返回值.没有指定的方法调用返回空值:

  • object返回null
  • 数值类型返回0
  • boolean返回false
  • 集合将返回空集合
  • ......

以下的断言语句仅用于演示目的. 真正的测试应该用模拟对象来测试另一些功能.

"when thenReturn"和"when thenThrow"

模拟对象可以根据传入方法中的参数来返回不同的值, when(….).thenReturn(….)方法是用来根据特定的参数来返回特定的值.

when(….).thenReturn(….)

我们也可以使用像anyString或者anyInt 这样的方法来定义某个依赖数据类型的方法返回特定的值.

如果指定了多个值,他们将按照顺序返回多个值.

请参照下方示例:

    @Test
    public void test1()  {
        //  创建mock对象
        MyClass test = mock(MyClass.class);
        // 定义getUniqueId()方法返回特定的值
        when(test.getUniqueId()).thenReturn(43);
        // 执行测试
        assertEquals(test.getUniqueId(), 43);
    }

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

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

    // 返回值独立于输入值
    @Test
    public void testReturnValueInDependentOnMethodParameter()  {
        Comparable<Integer> c= mock(Comparable.class);
        when(c.compareTo(anyInt())).thenReturn(-1);
        //assert
        assertEquals(-1, c.compareTo(9));
    }

    // 根据提供参数的类型返回特定的值
    @Test
    public void testReturnValueInDependentOnMethodParameter2()  {
        Comparable<Todo> c= mock(Comparable.class);
        when(c.compareTo(isA(Todo.class))).thenReturn(0);
        //assert
        assertEquals(0, c.compareTo(new Todo(1)));
    }

when(….).thenReturn(….)也可以用来抛出异常

Properties properties = mock(Properties.class);
when(properties.get("Anddroid")).thenThrow(new IllegalArgumentException(...));
try {
    properties.get("Anddroid");
    fail("Anddroid is misspelled");
} catch (IllegalArgumentException ex) {
    // good!
}

"doReturn when" 和 "doThrow when"

doReturn(…).when(…)的方法调用和when(….).thenReturn(….)类似.对于调用过程中抛出的异常非常有用.而doThrow则也是它的一个变体.

具体在Spy中使用.

使用Spy包装Java对象

可以使用@Spy注解 或者 spy() 方法来包装一个真实的对象. 除非有特殊的指定,否则每次调用都会委托给该对象.

示例如下:

public class SpyTest {
  
    @Test
    public void testLinkedListSpyWrong() {
        // 让我们来模拟一个LinkedList
        List<String> list = new LinkedList<>();
        List<String> spy = spy(list);
        
        // spy.get(0)将会调用真实的方法
        // 将会抛出 IndexOutOfBoundsException (list是空的)
        when(spy.get(0)).thenReturn("foo");
        assertEquals("foo", spy.get(0));
    }
    @Test
    public void testLinkedListSpyCorrect() {
        // 让我们来模拟一个LinkedList
        List<String> list = new LinkedList<>();
        List<String> spy = spy(list);
        // 必须使用doReturn来插桩
        doReturn("foo").when(spy).get(0);
        assertEquals("foo", spy.get(0));
    }
}

注: 在使用Spy包装真实对象时使用when(….).thenReturn(….)将无效,必须使用 doReturn(…).when(…)来进行插桩.

验证模拟对象的调用

Mockito将会追踪所有方法的调用和传入模拟对象的参数.你可以在模拟对象上使用verify()方法验证指定的条件是否满足.例如,你可以验证是否使用某些参数调用了方法.这种测试称为行为测试.行为测试并不能检查方法调用的结果,但是它可以验证一个方法是否使用正确的参数被调用.

示例如下:

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

        // 调用模拟对象的方法testing,并传入参数12
        test.testing(12);
        test.getUniqueId();
        test.getUniqueId();

        // 检查方法testing是否使用参数
        //12调用了
        verify(test).testing(ArgumentMatchers.eq(12));

        // 验证调用两次getUniqueId
        verify(test, times(2)).getUniqueId();

        // 也可以使用下面的方法来替代调用的次数
        verify(test, never()).someMethod("never called 从来没有调用");
        verify(test, atLeastOnce()).someMethod("called at least once 至少被调用一次");
        verify(test, atLeast(2)).someMethod("called at least twice 至少被调用5次");
        verify(test, times(5)).someMethod("called five times 被调用5次");
        verify(test, atMost(3)).someMethod("called at most 3 times 至多被调用3次");
        //下面的方法用来检查是否所有的用例都涵盖了,如果没有将测试失败
        //放在所有的测试后面
        verifyNoMoreInteractions(test);
    }
}

如果你并不关心输入值,可以使用anyXXX()方法,例如,anyInt(), anyString()或者any(Your.class)等等方法.

@InjectMocks进行依赖注入

我们可以使用@InjectMocks注解根据类型对构造方法,普通方法和字段进行依赖注入.

假设你有下面的类.

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

    public ArticleManager(User user, ArticleDatabase database) {
        super();
        this.user = user;
        this.database = database;
    }

    public void initialize() {
        database.addListener(new ArticleListener());
    }
}

这个类可以通过Mockito构建,并且它的依赖关系可以通过模拟对象来实现,下面的代码就演示这一关系:

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {
    @Mock
    ArticleCalculator calculator;
    @Mock
    ArticleDatabase database;
    @Mock
    User user;
    @InjectMocks
    private ArticleManager manager; //①

    @Test
    public void shouldDoSomething() {
        //使用了一个ArticleListener实例调用了addListener
        manager.initialize();
        // 验证database调用使用了ArticleListener类型的参数调用了addListener
        verify(database).addListener(any(ArticleListener.class));
    }

}
  1. 这一步创建了一个ArticleManager实例并注入到了模拟对象中.

Mockito可以通过构造方法注入,setter注入和属性注入的顺序来注入模拟对象(mock).因此如果ArticleManager的构造方法只包含User, 并且这两个字段都有setter,那这种情况下只有User的模拟对象会被注入.

捕获参数

ArgumentCaptor类允许在验证的时候可以访问到方法的调用参数,并用于测试.

下面的示例需要添加依赖: https://mvnrepository.com/artifact/org.hamcrest/hamcrest-library

public class MockitoTests {

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

    @Captor
    private ArgumentCaptor<List<String>> captor;
  
    @Test
    public final void shouldContainCertainListItem() {
        List<String> asList = Arrays.asList("someElement_test", "someElement");
        final List<String> mockedList = mock(List.class);
        mockedList.addAll(asList);

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

Answer的使用

在写测试用例时针对复杂的方法结果往往会使用Answer.虽然使用thenReturn可以每次返回一个预定义的值,但是通过answers可以让你的插桩方法(stubbed method)根据参数计算出结果.

例如,下面是使用Answer实现插桩方法返回第一个参数值的示例:

//假设存在这么一个类(仅为测试,毫无意义)
class TestObj {
    public String add(String firstArg, String lastArg) {
        return "";
    }
}
//...
@Test
public final void answerTest() {
    TestObj testObj = mock(TestObj.class);
    // with doAnswer():
    doAnswer(returnsFirstArg()).when(testObj).add(anyString(), anyString());
    // with thenAnswer():
    when(testObj.add(anyString(), anyString())).thenAnswer(returnsFirstArg());
    // with then() alias:
    when(testObj.add(anyString(), anyString())).then(returnsFirstArg());
    //测试打印结果
    System.out.println(testObj.add("FirstArg", "LastArg"));
}

打印结果:

FirstArg

有的时候你可能需要一个回调作为方法参数:

@Test
public final void callbackTest() {
    ApiService service = mock(ApiService.class);
    when(service.login(any(Callback.class))).thenAnswer(new Answer<Object>() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Callback callback = invocation.getArgument(0);
            callback.notify("Success");
            return "Test Result";
        }
    });

    String result = service.login(new Callback() {
        @Override
        public void notify(String notify) {
            System.out.println(notify);
        }
    });
    System.out.println(result);
}

打印结果:

Success
Test Result

甚至可以模拟一个持久服务,比如Dao, 但是如果Answers非常复杂应该考虑创建一个fake 类而不是mock.

@Test
public final void TestDao() {
    List<User> userMap = new ArrayList<>();
    UserDao dao = mock(UserDao.class);
    when(dao.save(any(User.class))).thenAnswer(i -> {
        User user = i.getArgument(0);
        userMap.add(user.getId(), user);
        return null;
    });
    when(dao.find(any(Integer.class))).thenAnswer(i -> {
        int id = i.getArgument(0);
        return userMap.get(id);
    });
}

模拟 final class

自从Mockito v2 以来可以模拟final class, 这个功能目前正在优化阶段,并且默认是停用的.要想激活final class,在src/test/resources/mockito-extensions/或者src/mockito-extensions/目录创建名为org.mockito.plugins.MockMaker的文件,并在文件中添加一行:

mock-maker-inline

如图所示:

Mockito支持final class 路径

测试代码:

final class FinalClass {
    public final String finalMethod() { return "something"; }
}

@Test
public final void mockFinalClassTest() {
     FinalClass instance = new FinalClass();

     FinalClass mock = mock(FinalClass.class);
     when(mock.finalMethod()).thenReturn("that other thing");

     assertNotEquals(mock.finalMethod(), instance.finalMethod());
}

当然,如果你不这么做,编译器将会抛出异常:

Mockito cannot mock/spy because :
– final class

Mockito一些重要的Api暂时就介绍到这里, 更多Api请移步: https://github.com/hehonghui/mockito-doc-zh/blob/master/README.md#0

敲行代码再睡觉
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容