天天看点

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 {

    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 {

    User queryByUsername(String username);

    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 的一些简单的基础知识,一些复杂的高级的功能另文介绍。

作者:金融测试民工

链接:https://www.jianshu.com/p/b8a52260dc22

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。