Mock和Java单元测试中的Mock框架Mockito介绍

什么是Mock?

    在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象。程序员通常创造模拟对象(mock object)来测试其他对象的行为,很类似汽车设计者使用碰撞测试假人来模拟车辆碰撞中人的动态行为。

为什么要使用Mock?

    在单元测试中,模拟对象可以模拟复杂的、真实的(非模拟)对象的行为, 如果真实的对象无法放入单元测试中,使用模拟对象就很有帮助。

在下面的情形,可能需要使用模拟对象来代替真实对象:

真实对象的行为是不确定的(例如,当前的时间或当前的温度);

真实对象很难搭建起来;

真实对象的行为很难触发(例如,网络错误);

真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);

真实的对象是用户界面,或包括用户界面在内;

真实的对象使用了回调机制;

真实对象可能还不存在;

真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。

    例如,一个可能会在特定的时间响铃的闹钟程序可能需要外部世界的当前时间。要测试这一点,测试一直要等到闹铃时间才知道闹钟程序是否正确地响铃。如果使用一个模拟对象替代真实的对象,可以变成提供一个闹铃时间(不管是否实际时间),这样就可以隔离地测试闹钟程序。

Mockito的简单使用

    Mockito是GitHub上使用最广泛的Mock框架,并与JUnit(java单元测试框架)结合使用。Mockito框架可以创建和配置mock对象.使用Mockito简化了具有外部依赖的类的测试开发!

一般使用Mockito的步骤:

1、模拟任何外部依赖并将这些模拟对象插入测试代码中

2、执行测试中的代码

3、验证代码是否按照预期执行

    单元测试是每个程序员的一项基本技能,甚至于还出现一种 TDD 的敏捷软件设计开发方法。在我们划分好模块进行详细设计编码之前,可能只是粗略的定义了一些接口,在我们进行的前后端分离开发方式实践中,以及微服务架构的系统设计中,经常会遇到这种情况。

    在我们需要测试的代码所依赖的服务还未实现,或者说要构建依赖的对象比较困难时,使用Mock的方式进行单元测试是一种比较好的选择。例如我们在使用 Spring 框架开发和测试 Service 层的代码时,并不需要等到 Dao 层的相关代码开发完成才进行单元测试。

    本文主要介绍Java编程领域一个非常好用的Mock框架的应用。

    1、Mockito的引入

    Mockito 目前发布的是 2.x 版本( Mockito 3.x 版本目前还在开发中,会考虑 Java 8 的一些新特性)。我们以 Maven 为例(当然根据自己项目的情况也可以使用 Ivy、Gradle、SBT 等等,甚至直接把 jar 包下载下来放到项目中使用),只需要在 Maven 项目的 pom 文件中增加 Mockito 依赖即可。

<dependency>

    <groupId>org.mockito</groupId>

    <artifactId>mockito-core</artifactId>    

    <version>2.23.0</version>

</dependency>

       2、第一段Mock测试代码。

       考虑一个简单的用户注册功能,我们需要先判断注册用户的用户名是否被其他用户注册过。如果已被注册过,则注册失败,如果未被注册过,则保存注册信息,注册成功。下面是代码设计(示例代码使用 spring 框架,并使用了 lombok 以减少 POJO 类的 getter,setter 定义):

@Data

public class User {

    private String idUser;      // 用户ID

    private String username;    // 用户名

    private String password;    // 用户密码

}

public interface UserService {

    /**

    * 新用户注册。注册成功返回true,注册失败返回false

    * @param user 新注册用户

    * @return

    */

    boolean regist(User user);

}

@Service

public class UserServiceImpl implements UserService {

    @Autowired

    private UserDao userDao;

    @Override

    public boolean regist(User user) {

        User existUser = userDao.queryByUsername(user.getUsername());

        if (existUser == null) {

            userDao.insertUser(user);

            return true;

        }

        return false;

    }

}

public interface UserDao {

    /**

    * 根据用户名查询用户

    * @param username 用户名

    * @return

    */

    User queryByUsername(String username);

    /**

    * 持久化新用户

    * @param user 新用户

    * @return

    */

    void insertUser(User user);

}

    这个时候 UserDao 的实现类还没有开发,我们要测试 UserService 的 regist 方法时就可以使用 Mock 了。下面就是第一段使用 JUnit 结合 Mockito 编写的单元测试代码。

@RunWith(MockitoJUnitRunner.class)

public class UserServiceImplTest {

    String existUsername    = "spiderman";

    String notExistUsername = "ironman";

    @Mock

    private UserDao        userDao;

    @InjectMocks

    private UserServiceImpl userService;

    @Before

    public void setUp() throws Exception {

        // 效果同@RunWith(MockitoJUnitRunner.class)

        // MockitoAnnotations.initMocks(this);

        User existUser = new User();

        existUser.setUsername(existUsername);

        existUser.setPassword("aaaaa");

        // 当调用userDao.queryByUsername入参为"spiderman"时会返回existUser对象,表示该用户已存在

        Mockito.when(userDao.queryByUsername(existUsername)).thenReturn(existUser);

        // 当调用userDao.queryByUsername入参为"ironman"时会返回null值,表示不存在该用户

        Mockito.when(userDao.queryByUsername(notExistUsername)).thenReturn(null);

    }

    @Test

    public void testExists() throws Exception {

        User testUser = new User();

        testUser.setUsername(existUsername);

        Assert.assertFalse(userService.regist(testUser));

    }

    @Test

    public void testNotExists() throws Exception {

        User testUser = new User();

        testUser.setUsername(notExistUsername);

        Assert.assertTrue(userService.regist(testUser));

    }

}

以上单元测试代码除了几行带有 Mock 字样的代码,其他内容和我们之前写的单元测试没有区别。从面的代码我们可以看到 userService 依赖了 userDao ,但是我们的代码中并没有实例化这两个对象,并且源代码中也没有 UserDao 的具体实现,但是我们的测试代码却可以像是已经实例化了这两个对象一样进行操作。下面我们就来看看这几行新增代码的作用。

       3、Mock注解介绍。

       @RunWith(MockitoJUnitRunner.class) 该注解会在test方法执行之前初始化使用 @Spy & @Mock & @InjectMocks 注解的对象;该注解还会自动验证我们单元测试用Mockito框架的使用情况。如果我们在调用Mockito的静态方法when之后继续链式调用相应的 stub 方法(如上面示例代码中去掉thenReturn方法调用),单元测试代码可以编译通过,是运行时会报错。

我们在setup方法中第一行使用MockitoAnnotations.initMocks(this)可以达到该注解同样的效果。

       当然我们也可以使用 Mockito.mock 方法手动创建 mock 对象,但是并不推荐这样做。

       @Mock 该注解表示会创建一个mock对象。我们在该 mock 对象上的方法调用并不会实际调用具体的实现代码(也可能其实本来就还没有实现)

       @Spy 该注解上面示例代码并未使用,功能与 @Mock 类似,也是创建一个mock对象,区别在于调用 @Spy 对应的mock对象上的方法时,会实际调用事实实现好的代码(如果已有实现方法的前提下),但是不会影响我们when-then语句中的定义。但是使用该注解还是会为我们省去对象创建的过程。例如上面示例中如果我们实现了 UserDao 接口:

@Repository

public class UserDaoImpl implements UserDao {

    @Override

    public User queryByUsername(String username) {

        System.out.println("call UserDaoImpl.queryByUsername");

        return null;

    }

    @Override

    public void insertUser(User user) {

    }

}

然后更换单元测试类型的 Mock定义:

@Mock

private UserDao userDao;

更换为

@Spy

private UserDaoImpl userDao;

执行单元测试后我们可以在控制台看到 UserDaoImpl 实现方法中的打印语句:

如果我们再把 @Spy 注解切换回  @Mock 注解,可以发现控制台不会打印 UserDaoImpl 实现方法中的打印语句。

@Spy

privateUserDaoImpl userDao;

更换为

@Mock

privateUserDaoImpl userDao;

@InjectMocks 该注解表示会创建一个测试类的实例,并注入依赖的mock对象(@Mock 注解或 @Spy 注解)。

       4、Mock的应用介绍

       除了Mock的注解,下面我们再来看看使用Mock的表达式。上面示例代码我们展示了when-then表达式的使用。我们使用该语句定义对象方法调用的一些预先约定。

       when方法定义方法场景,指定了具体的mock对象,指定了mock对象的某个具体调用方法,指定了该方法的调用参数值(如果不关心具体的参数值内容,可以用Any代替)。

       then方法定义了我们约定的方法调用之后需要具体执行的操作,比如返回一个值或者抛出一个异常。

    另外我们还可以使用Mock做一些验证。例如我们在Assert的断言方法后面增加一些判断,如果测试用例是注册不存在的用户,我们的业务逻辑中会调用userDao的insertUser方法,这个时候我们可以增加一行Mockito.verify(userDao).insertUser(testUser)。如果我们的 UserServiceImpl 实现中去掉userDao.insertUser(user)调用,测试不会通过,也就提示我们说新注册的用户没有持久化操作需要修复 UserServiceImpl 里的实现逻辑。

       Mockito在 stackoverflow 是程序员投票最广泛使用评价最高的一个 Java 编程 Mock 框架。使用该框架编写的单元测试代码美观、清洁、易于理解,并且功能强大。本文只是简单介绍 Mockito 的一些简单的基础知识,一些复杂的高级的功能另文介绍。

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