单元测试是每个程序员的一项基本技能,甚至于还出现一种 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 的一些简单的基础知识,一些复杂的高级的功能另文介绍。
网友评论