安卓单元测试(九):使用Mockito Annotation快速创建Mock

注:
如果你还不了解Mock的概念或Mockito框架的使用,请先看这篇文章

@Mock的基本用法

如果你follow了这个安卓单元测试系列文章,那么到现在为止,你应该很清楚mock的概念和使用了,创建Mock的方法我们都知道:

YourClass yourInstance = Mockito.mock(YourClass.class);

比如:

public class LoginPresenterTest {

    @Test
    public void testLogin() {
        UserManager mockUserManager = mock(UserManager.class);
        PasswordValidator mockValidator = mock(PasswordValidator.class);
        Mockito.when(mockValidator.verifyPassword("xiaochuang is handsome")).thenReturn(true);

        LoginPresenter presenter = new LoginPresenter(mockUserManager, mockValidator);

        presenter.login("xiaochuang", "xiaochuang is handsome");

        verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome");
    }
}

虽然很简单,但是如果一个测试类里面很多测试方法都要用到mock,那写起来就会有点麻烦,这时候我们可以写一个@Before方法来作这个setup工作:

public class LoginPresenterTest {

    UserManager mockUserManager;
    PasswordValidator mockValidator;
    LoginPresenter loginPresenter;

    @Before
    public void setup() {
        mockUserManager = mock(UserManager.class);
        mockValidator = mock(PasswordValidator.class);
        loginPresenter = new LoginPresenter(mockUserManager, mockValidator);
    }

    @Test
    public void testLogin() {
        Mockito.when(mockValidator.verifyPassword("xiaochuang is handsome")).thenReturn(true);
        loginPresenter.login("xiaochuang", "xiaochuang is handsome");

        verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome");
    }
}

这样可以部分上减少Mock的创建,然而Mock写多了,你也会觉得有点烦,因为完全是Boilerplate code。这里有个更简便的方法,那就是结合Mockito的Annotation和JUnit Rule,达到以下的效果:

public class LoginPresenterTest {

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

    @Mock
    UserManager mockUserManager;

    @Mock
    PasswordValidator mockValidator;

    LoginPresenter loginPresenter;

    @Before
    public void setup() {
        loginPresenter = new LoginPresenter(mockUserManager, mockValidator);
    }

    @Test
    public void testLogin() {
        Mockito.when(mockValidator.verifyPassword("xiaochuang is handsome")).thenReturn(true);
        loginPresenter.login("xiaochuang", "xiaochuang is handsome");

        verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome");
    }
}

也就是说,在测试方法运行之前,自动把用@Mock标注过的field实例化成Mock对象,这样在测试方法里面就可以直接用了,而不用手动通过Mockito.mock(YourClass.class)的方法来创建。这个实现化的过程是在MockitoRule这个Rule里面进行的。我们知道(不知道?)一个JUnit Rule会在每个测试方法运行之前执行一些代码,而这个Rule实现的效果就是在每个测试方法运行之前将这个类的用了@Mock修饰过的field初始化成mock对象。如果你去看这个Rule的源代码的话,其实重点就在一行代码:

MockitoAnnotations.initMocks(target);

上面的target就是我们的测试类(LoginPresenterTest)。所以,在上面的例子中,如果你不使用这个Rule的话,你可以在@Before里面加一行代码:

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
    loginPresenter = new LoginPresenter(mockUserManager, mockValidator);
}

也能达到一样的效果。

使用@InjectMocks

其实在上面的代码中,我们还可以进一步的简化。我们可以使用@InjectMocks来让Mockito自动使用mock出来的mockUserManagermockValidator构造出一个LoginPresenter

public class LoginPresenterTest {

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

    @Mock
    UserManager mockUserManager;
    @Mock
    PasswordValidator mockValidator;

    @InjectMocks
    LoginPresenter loginPresenter;

    @Test
    public void testLogin() {
        Mockito.when(mockValidator.verifyPassword("xiaochuang is handsome")).thenReturn(true);
        loginPresenter.login("xiaochuang", "xiaochuang is handsome");

        verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome");
    }
}

这是因为Mockito在MockitoAnnotations.initMocks(this);是时候,看到这个被@InjectMocksLoginPresenter有一个构造方法:

    public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) {
        this.mUserManager = userManager;
        this.mPasswordValidator = passwordValidator;
    }

这个构造方法所需要的两个参数正好这个测试类里面有这样的field被@Mock修饰过。于是就用这两个field来作为LoginPresenter的构造参数,将LoginPresenter构造出来了。这个叫做Constructor Injection
field的声明顺序其实是无所谓的,你完全可以把

    @InjectMocks
    LoginPresenter loginPresenter;

放在两个@Mock field前面。此外,如果你觉得每个测试类里面都要写这个Rule有点麻烦,你可以建一个Test基类,然后把这个Rule的定义放在基类里面,每个Test类继承这个基类,这样就不用每个测试类自己写这个Rule了。

诡异的@InjectMocks

其实,@InjectMocks是比较tricky的一个东西。比如说,如果LoginPresenter的构造方法是空的,就是没有参数:

public class LoginPresenter {
    private UserManager mUserManager;
    private PasswordValidator mPasswordValidator;

    public LoginPresenter() {
    }
}

那么Mockito依然会将LoginPresenter里面的mUserManagermPasswordValidator初始化为LoginPresenterTest里面的两个mock对象,mockUserManagermockValidator。这个效果跟LoginPresenter有两个构造参数是一样的。也就是说,如果被@InjectMocks修饰的field只有一个默认的构造方法,那么在inject mocks的时候,Mockito会去找被@InjectMocks修饰的field的类(这里是LoginPresenter)的field,然后根据类型对应的初始化为测试类里面用@Mock修饰过的field,这个叫Field Injection。然而如果LoginPresenter只有一个UserManager的构造方法:

public class LoginPresenter {
    private UserManager mUserManager;
    private PasswordValidator mPasswordValidator;

    public LoginPresenter(UserManager userManager) {
        this.mUserManager = userManager;
    }
}

那么,在上面的LoginPresenterTest例子里面,只有mUserManager会被初始化为mockUserManager, 而mPasswordValidator是不会初始化为mockValidator的。这就是tricky的地方。
此外还有Property Setter Injection,也就是通过setter方法,自动的初始化@InjectMocks对象。这个要搞清楚具体的工作流程还是有点小复杂的。这三种injection的优先级顺序分别为:
Constructor Injection > Property Setter Injection > Field Injection。 具体情况可以在这里看到。很多人其实都不推荐使用@InjectMocks,因为很难弄清楚到底会通过哪种方式inject,我个人对这点也感觉有点别扭,我宁愿通过new LoginPresenter(mockUserMananger, mockValiator)这种方式来创建LoginPresenter对象,而不是使用@InjectMocks

使用@Spy创建Spy对象

介绍Mock的时候,我们还介绍了Spy,同样的,我们可以使用@Spy来快速创建spy对象。

@Spy PasswordValidator spyValidator;
//or
@Spy PasswordValidator spyValidator = new PasswordValidator();

当然,就跟使用Mockito.spy()方法创建Spy对象一样,要么用@Spy修饰的field的类有默认的Constructor,要么是一个对象。如果PasswordValidator没有默认无参的构造方法,那么@Spy PasswordValidator spyValidator;这个方式是会报错的。

小结

本以为这篇文章应该会很短,没想到写出来也不短了,这就跟做一个新feature一样,乍一看好像很简单,一个小时搞定,结果两个小时以后,呃。。。

照例文中的代码在github的这个repo

获取最新文章或想加入安卓单元测试交流群,请关注下方公众号


努力写出高质量的文章
希望能通过自己的努力,写出高质量的文章

Every minute count!

490 声望
100 粉丝
0 条评论
推荐阅读
浅谈App的启动优化
温启动:当启动应用时,后台已有该应用的进程,但是Activity可能因为内存不足被回收。这样系统会从已有的进程中来启动这个Activity,这个启动方式叫温启动。

xuexiangjys5阅读 1.6k

Android-Lifecycle超能解析-生命周期的那些事儿
版权声明:本文已授权微信公众号:Android必修课,转载请申明出处众所周知,Android凡是需要展示给用户看的,都包含着生命周期这个概念,例如Activity、Fragment、View等都与生命周期息息相关,在生命周期函数里...

XBaron4阅读 6.8k

布局大杀器—ConstraintLayout
Hi,大家好,看到标题后大家是不是一脸懵逼,这是啥?这小编搞事情?说好的六大布局咋又来个布局杀手?这就是咱们公众号和其他公众号的不同,我们并不是照本宣科的讲解Android知识,而是将项目当中实际运用到的并...

下码看花1阅读 3.9k

Android-博客及公众号推荐
首先强烈的推荐 stormzhang的博客,一直在关注他的博客和公众号,对我影响很大,不仅仅是Android学习之路,而且还是还会分享一些Android或者人生的经验,我也是一步步靠自己走过来,还在继续努力中。看stormzhang...

秦子帅2阅读 3.9k

六大布局之RelativeLayout
上一期我们给大家讲解了FrameLayout的使用,这一期我们为大家讲解一下RelativeLayout(相对布局)的使用,RelativeLayout是Android的六大布局之一,也是我们常用的布局之一,下面我们一起开始学习吧~

下码看花1阅读 5.9k

Android安全之Intent Scheme Url攻击
0X01 前言Intent scheme url是一种用于在web页面中启动终端app activity的特殊URL,在针对intent scheme URL攻击大爆发之前,很多android的浏览器都支持intent scheme url。Intent scheme url的引入虽然带来了一...

YAQ御安全1阅读 9.1k

Android常用的网络框架
一、Android 常用的网络框架大多数应用程序基本都需要连接网络,发送一些数据给服务端,或者从服务端获取一些数据。通常在 Android 中进行网络连接一般使用 Scoket 和HTTP,HTTP 请求方式比 Scoket 多得多。HTTP ...

위엄위엄1阅读 3.2k

Every minute count!

490 声望
100 粉丝
宣传栏