使用 Mockito 的 @InjectMocks 创建被测试类实例

初识 Mockito 这个测试框架后,我们要使用 Mock 的属性创建一个被测试类实例时,大概会下面这么纯手工来打造。

假定类 UserService 有一个属性 UserDao userDao, 需要构造 UserService 实例时 Mock 内部状态

UserDao userDao = Mockito.mock(UserDao.class);
UserService testMe = new UserService(userDao);

如此,userDao 的行为就可以自由模拟了,这种纯手工方式都不需要给测试类添加

@RunWith(MockitoJunitRuner.class)
//或
MockitoAnnotations.initMocks(this);

因为上面两句是给 Mockito 的注解使用的。

如果所有的 Mock 对象全部通过手工来创建,那就不容易体现出 Mockito 的优越性出来。因此对于被测试对象的创建,Mock 属性的注入应该让 @Mock 和 @InjectMocks 这两个注解大显身手了。

标注在实例变量上的 @Mock 相当于是 Mockito.mock(Class) 创建了一个 Mock 对象,而 @InjectMock 标的实例会寻找到相应 Mock 属性想法构造出被测试类的实例。看下面的例子:

UserService 类

public class UserService {
 

    private UserDao userDao;

 

    public UserService(UserDao userDao) {
        System.out.println("Constructor called");

        this.userDao = userDao;

    }

 

    public UserDao getUserDao() {
        return userDao;

    }

}

UserServiceTest 类

@RunWith(MockitoJUnitRunner.class)

public class UserServiceTest {
 

    @Mock

    private UserDao userDao;


    @InjectMocks

    private UserService testMe;

 
    @Test

    public void testInjectMocks() {
        System.out.println(testMe.getUserDao().getClass());

    }

}


Constructor called上面测试用例的输出为

class cc.unmi.UserDao$MockitoMock$878185941

证明了 Mock 对象 userDao 成功的通过构造函数注入了 testMe 实例。

除了通过构造函数注入 Mock 的属性外, @InjectMocks  还能通过 setter 方法,属性注入。私有的构造函数,setter 方法,属性都无法阻止 @InjectMocks 注入 Mock 对象。

下面是理解自 Mockito 官方对 @InjectMocks 的 JavaDoc 说明,链接:InjectMocks - mockito-core 2.13.0 javadoc

  1. Mockito 尝试按 非默认构造函数setter 方法属性 的顺序来注入 Mock 对象。如果存在一个有参数的构造函数,那么 setter 方法 和 属性  注入都不会发生。也就是说 非默认构造函数 不会与后两种方式同时发生,但找不到 setter 注入的 Mock 对象还会尝试用 属性 来直接注入。
  2. 如果 @InjectMocks 对象只有默认构造数,那么会调用该默认构造函数,并且依次采用下面两种方式注入属性。
  3. 非默认构造函数注入: Mockito 会选择参数个数最多的构造函数(称之为最大构造函数) -- 这样可以尽可能注入多的属性。但是有多个最大构造函数,Mockito 究竟选择哪一个就混乱,测试时应该避免这种情况的发生。
  4. 如果构造函数中含有不可 Mock 的参数(基本类型), 则该构造函数将被 @InjectMocks 忽略掉。
  5. setter 方法注入: 和 Spring 类似,Mockito 首先根据属性类型(或擦除类型)找到 Mock 对象。存在多个相同类型 Mock 对象则按名称(@Mock(name="userDao1"))进行匹配,默认名称为空。不能按名称匹配到的话,可能会选择最后声明的那个,不确定性。
  6. 属性 注入: 按 Mock 对象的类型或是名称的匹配规则与 setter 方法注入 是一样的。

现在来开始有事实验证上面理解的 @InjectMocks 理论:

调用最大构造函数,调用了非默认构造函数将不会采用 setter 方法 和 属性 注入

public class UserService {
    public UserDao userDao;

 

    private UserService(String s1) {
        System.out.println("Constructor 1 called");

    }

 

    private UserService(String s1, String s2) {
        System.out.println("Constructor 2 called");

    }

 

    public void setUserDao(UserDao userDao) {
        System.out.println("call setter");

        this.userDao = userDao;

    }

}

 

@RunWith(MockitoJUnitRunner.class)

public class UserServiceTest {
    

    @Mock

    private UserDao userDao;

 

    @InjectMocks

    private UserService testMe;

 

    @Test

    public void testInjectMocks() {
        System.out.println(testMe.userDao);

    }

}

上面测试执行输出为:

Constructor 2 called
null

同时证明了私有的构造函数一样被调用。

@InjectMocks 调用了默认构造函数后还能同时应用 setter 方法 和 属性 注入两种式

public class UserService {


    public UserDao userDao;

    private BookDao bookDao;



    public UserService() {

        System.out.println("Constructor 0 called");

    }



    private void setUserDao(UserDao userDao) {

        System.out.println("call setter");

        this.userDao = userDao;

    }


    public BookDao getBookDao() {

        return this.bookDao;

    }

}



@RunWith(MockitoJUnitRunner.class)

public class UserServiceTest {


    @Mock
    private UserDao userDao;


    @Mock
    private BookDao bookDao;



    @InjectMocks

    private UserService testMe;


    @Test
    public void testInjectMocks() {

        System.out.println(testMe.userDao.getClass());

        System.out.println(testMe.getBookDao().getClass());

    }

}

测试代码输出如下:

Constructor 0 called
class cc.unmi.UserDao$MockitoMock$1978393893
class cc.unmi.BookDao$MockitoMock$910006861

默认构造函数调用了,userDao 通过  setter 方法注入的,bookDao 通过属性直接注入的。把 setUserDao(..) 方法和 bookDao  设置为私有也是为了证明可见性不是障碍,当然 public 的更不是事。

含有基本类型参数的构造函数将被 @InjectMocks 忽略掉

public class UserService {


    public UserDao userDao;


    public UserService() {

        System.out.println("Constructor 0 called");

    }


    private UserService(UserDao userDao, boolean flag) {

        System.out.println("Constructor 2 called");

    }

}



@RunWith(MockitoJUnitRunner.class)

public class UserServiceTest {


    @Mock

    private UserDao userDao;


    @InjectMocks

    private UserService testMe;


    @Test

    public void testInjectMocks() {

        System.out.println(testMe.userDao.getClass());

    }

}

执行测试用例的输出为:

Constructor 0 called
class cc.unmi.UserDao$MockitoMock$286493746

由于无法构造出 Mock 的 boolean 类型,所以 UserService(UserDao userDao, boolean flag) 被忽略,调用了默认构造函数,并且 userDao 通过属性进行了注入。

多个相同类型的 Mock 对象通过名称进行匹配

public class UserService {

    public UserDao userDao2;

    private UserService(UserDao userDao1, String abc) {

        System.out.println("Constructor 2 called");

        this.userDao2 = userDao1;

    }
}


@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {


    @Mock(name = "userDao1")
    private UserDao userDao1;


    @Mock(name = "userDao2")
    private UserDao userDao2;


    @InjectMocks
    private UserService testMe;


    @Test

    public void testInjectMocks() {

        Assert.assertEquals(userDao1, testMe.userDao2);

    }

}


输出为:

Constructor 2 called

UserService 类中对 userDao2 和 userDao1 名称进行错位安排是为了证明名称匹配是根据注入点处的名称对比的。例如

  1. 构造函数注入,根据参数名进行匹配
  2. setter 方法注入,根据 setter 方法名, 如 setUserDao1(..), 或 setUserDao2(..) 匹配的,与方法参数无关
  3. 属性注入自然是以属性名本身为准

同时该例也证明了构造函数 UserService(UserDao userDao1, String abc) 对 @InjectMocks 是可见的,因为 String 是非基本类型,也是可以 Mock String 类型的。

因此,需要我们留意的是,产品代码构造函数的变动可能会改变测试代码的行为,或是导致测试的失败。

@InjectMocks 只能注入 Mock 对象,例如以下均是 Mock 对象

  1. UserDao userDao = Mockito.mock(UserDao.class);
  2. @Mock private UserDao userDao;
  3. @Mock private UserDao userDao = new UserDao();    //Mockito 将会对 userDao 重新赋值为一个  Mock 对象
  4. UserDao userDao = spy(new UserDao());

如果是一个普通对象,例如下面的声明

private UserDao userDao = new UserDao();

@InjectMocksprivate UserService testMe;


@InjectMocks 如何费尽心思都无法把这个  userDao  注入到 testMe  测试对象中去的。对它 spy 一下就可以被注入了。

@Mock 和 @InjectMocks 会把自己赋的值丢弃

前面提到 @Mock private UserDao userDao = new UserDao(); 最终的 userDao 是一个 Mock  对象,@InjectMocks  也一样

@InjectMocks

private UserService testMe = new UserService(); 

虽然会调用一下 new UserService() 创建一个对象,但最终的值是由 @InjectMocks 产生的。


备注一个使用 @Mock 对象创建被测试实例的错误

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {


    @Mock
    private UserDao userDao;


    private UserService testMe = new UserService(userDao); //此时 userDao 还是 null



    @Before
    public void setup() {

        testMe = new UserService(userDao); //这里的 userDao 才是一个 Mock 对象

    }

}

静态测试类的示例

@RunWith(PowerMockRunner.class)
@PrepareForTest({
        SpringContext.class,KeywordRuleCacheData.class
})
public class KeywordRuleCacheDataRefreshDealTest {

    @InjectMocks
    KeywordRuleCacheDataRefreshDeal keywordRuleCacheDataRefreshDeal;

    @Test
    public void run() throws BaseAppException {
        PowerMockito.mockStatic(SpringContext.class);
        PowerMockito.mockStatic(KeywordRuleCacheData.class);
        PowerMockito.when(KeywordRuleCacheData.refushKeywordRuleCacheData()).thenReturn(true);
        keywordRuleCacheDataRefreshDeal.run();
    }

}

更多推荐

Java测试工具Mock详解