Java测试工具Mock详解

简介: Java测试工具Mock详解

使用 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();
    }
}


相关文章
|
25天前
|
Java 测试技术 开发者
必学!Spring Boot 单元测试、Mock 与 TestContainer 的高效使用技巧
【10月更文挑战第18天】 在现代软件开发中,单元测试是保证代码质量的重要手段。Spring Boot提供了强大的测试支持,使得编写和运行测试变得更加简单和高效。本文将深入探讨Spring Boot的单元测试、Mock技术以及TestContainer的高效使用技巧,帮助开发者提升测试效率和代码质量。
136 2
|
15天前
|
Java 测试技术 Maven
Java一分钟之-PowerMock:静态方法与私有方法测试
通过本文的详细介绍,您可以使用PowerMock轻松地测试Java代码中的静态方法和私有方法。PowerMock通过扩展Mockito,提供了强大的功能,帮助开发者在复杂的测试场景中保持高效和准确的单元测试。希望本文对您的Java单元测试有所帮助。
32 2
|
22天前
|
Java 程序员 测试技术
Java|让 JUnit4 测试类自动注入 logger 和被测 Service
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
22 5
|
1月前
|
Java 流计算
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
37 1
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
|
28天前
|
存储 人工智能 Java
将 Spring AI 与 LLM 结合使用以生成 Java 测试
AIDocumentLibraryChat 项目通过 GitHub URL 为指定的 Java 类生成测试代码,支持 granite-code 和 deepseek-coder-v2 模型。项目包括控制器、服务和配置,能处理源代码解析、依赖加载及测试代码生成,旨在评估 LLM 对开发测试的支持能力。
36 1
|
2月前
|
测试技术 数据库连接 数据库
提升软件测试效率与灵活性:探索Mock测试的重要性
【9月更文挑战第20天】在软件测试领域,提升测试效率与灵活性至关重要。Mock 测试通过模拟外部组件,使模块能独立测试,缩短测试周期;快速反馈机制让测试结果即时可见,加速问题修复;还能模拟异常情况和进行参数化测试,增强测试全面性与灵活性,从而显著提高软件质量和开发效率。
|
1月前
|
分布式计算 Java 大数据
大数据-122 - Flink Time Watermark Java代码测试实现Tumbling Window
大数据-122 - Flink Time Watermark Java代码测试实现Tumbling Window
32 0
|
2月前
|
SQL JavaScript 前端开发
基于Java访问Hive的JUnit5测试代码实现
根据《用Java、Python来开发Hive应用》一文,建立了使用Java、来开发Hive应用的方法,产生的代码如下
71 6
|
1月前
|
算法 Java 测试技术
数据结构 —— Java自定义代码实现顺序表,包含测试用例以及ArrayList的使用以及相关算法题
文章详细介绍了如何用Java自定义实现一个顺序表类,包括插入、删除、获取数据元素、求数据个数等功能,并对顺序表进行了测试,最后还提及了Java中自带的顺序表实现类ArrayList。
19 0
|
3月前
|
IDE Java 测试技术
揭秘Java高效编程:测试与调试实战策略,让你代码质量飞跃,职场竞争力飙升!
【8月更文挑战第30天】在软件开发中,测试与调试对确保代码质量至关重要。本文通过对比单元测试、集成测试、调试技巧及静态代码分析,探讨了多种实用的Java测试与调试策略。JUnit和Mockito分别用于单元测试与集成测试,有助于提前发现错误并提高代码可维护性;Eclipse和IntelliJ IDEA内置调试器则能快速定位问题;Checkstyle和PMD等工具则通过静态代码分析发现潜在问题。综合运用这些策略,可显著提升代码质量,为项目成功打下坚实基础。
62 2