当我们编写单元测试的过程中,我们常常遇到应用中其他依赖模块尚未开发完成,或者该依赖的构建比较复杂的情况,例如Service层已经开发完成,DAO层却还在开发当中,但Service需要依赖DAO来进行测试,显然这种情况下Service是没有办法进行测试的,因为此时需要依赖DAO进行测试,又或者例如测试Servlet,Request和Session等都需要由服务器来生成,而Mock对象就是用来对一些未实现的关联对象或依赖对象的类进行测试的对象,EasyMock就是实现Mock对象的框架,例如Service依赖DAO,我们可以使用Mock对象来模拟DAO的实现或者模拟Request和Session,简单可以理解为Mock对象是模拟了我们给定接口实现的对象
- <dependency>
- <groupId>org.easymock</groupId>
- <artifactId>easymock</artifactId>
- <version>3.2</version>
- <scope>test</scope>
- </dependency>
Mock对象的生命周期:
1.record --> 对Mock对象进行关系的说明,交互的过程和可能产生的结果
2.replay --> 进入了发布阶段,也就是测试阶段
3.verify --> 验证交互的关系是否正确
在我们开发的过程当中,应该首先定义的是接口,现在假设我们已经定义了DAO层的接口,但实现还没有开发出来,此时要进行Service层的测试,以下为DAO的接口
- package com.accentrix.ray;
- public interface UserDao {
- // 通过用户名获取用户
- User getBy(String username);
- // 添加用户
- void add(User user);
- }
以下为UserServiceImpl
- package com.accentrix.ray;
- public class UserServiceImpl implements UserService {
- private UserDao userDao;
- public UserServiceImpl() {
- }
- public UserServiceImpl(UserDao userDao) {
- this.userDao = userDao;
- }
- public User login(String username,String password) {
- if (username == null)
- throw new RuntimeException("用户名为空");
- if (password ==null)
- throw new RuntimeException("密码为空");
- User loginUser = userDao.getBy(username);
- if (loginUser == null)
- throw new RuntimeException("不存在用户");
- else
- if(!password.equals(loginUser.getPassword()))
- throw new RuntimeException("密码错误");
- else
- return loginUser;
- }
- public void add(User user) {
- if(user==null)
- throw new RuntimeException("用户为空");
- else
- userDao.add(user);
- }
- }
以下为测试类
可以留意到上面的testGet和testAdd获取Mock对象时,testGet是使用createMock,而testAdd()是使用createStrictMock,两者究竟有什么区别呢?就用testAdd中作为例子,testAdd中需要执行2次UserDao的方法,分别是add和get,此时若然使用createMock是不会检查他们之间的顺序,那么就可以先执行get再执行add,但使用createStrictMock就要检查顺序是否正确,而生命周期中的verify就是进行验证的操作.
- package com.accentrix.ray;
- import org.easymock.EasyMock;
- import org.easymock.IMocksControl;
- import org.junit.Assert;
- import org.junit.Before;
- import org.junit.Test;
- public class TestUserService {
- private User admin;
- private UserDao userDao;
- private UserService userService;
- @Before
- public void setUp() {
- // 初始化一个用户
- admin = new User(1, "Ray", "123");
- // 创建UserDao的Mock对象
- userDao = EasyMock.createMock(UserDao.class);
- userService = new UserServiceImpl(userDao);
- }
- @Test
- public void testLogin() {
- /*
- * 以下开始进入record阶段
- */
- // 此时说明当在Dao中调用get方式时并且参数为Ray的时候应该返回1个用户对象给我
- EasyMock.expect(userDao.getBy("Ray")).andReturn(admin);
- /*
- * 以下进入replay阶段
- */
- // 将使用userDao创建的Mock对象传入
- EasyMock.replay(userDao);
- // 直接调用 接口的方法
- User loginUser = userService.login("Ray","123");
- // 使用断言判断是否为空
- Assert.assertNotNull(loginUser);
- /*
- * 进入verify阶段
- */
- EasyMock.verify(userDao);
- }
- @Test
- public void testAdd() {
- /*
- * 首先理清测试add方法的思路,先通过UserDao插入一条数据
- * 然后通过UserDao的get方法来获取,验证是否成功插入
- */
- /*
- * 在EasyMock最新版本中,已经不推荐使用EasyMock.cerateMock()
- * 的方式创建Mock对象,而是采用以下方式,以下创建方法和testGet的
- * 效果完全一样,但注意此时创建的是StrictControl
- */
- IMocksControl mc = EasyMock.createStrictControl();
- userDao = mc.createMock(UserDao.class);
- /*
- * 当userDao的Mock对象是返回值,例如get(),getAll()的时候可以
- * 使用EasyMock.expect(),但当该方法没有返回值,例如add()的时候,
- * 应该要如何编写呢?请留意以下代码
- */
- userDao.add(admin);
- EasyMock.expectLastCall();
- /*
- * 为userDao注册两次事件,注意是必须的,有多少次交互,就需要有
- * 多少次注册,每次注册都必须对应
- */
- EasyMock.expect(userDao.getBy("Ray")).andReturn(admin);
- EasyMock.replay(userDao);
- userDao.add(admin);
- User addUser = userDao.getBy("Ray");
- Assert.assertNotNull(addUser);
- EasyMock.verify(userDao);
- /*
- * reset可以将Control进行重置,方便MockControl的重用,
- * 我们可以在@After中使用
- */
- mc.reset();
- }
- }
- @Test
- public void testAdd() {
- /*
- * 注意此处为createNiceControl,和createMockControl和createStrictMock
- * 的分别为,在使用NiceControl时若然调用的方法没有注册,仍然可以成功调用
- * ,不过会返回0,null,false的友好值,但不建议使用此种方式
- */
- IMocksControl mc = EasyMock.createNiceControl();
- userDao = mc.createMock(UserDao.class);
- // times代表注册2次getBy('Ray')的方法
- EasyMock.expect(userDao.getBy("Ray")).times(2);
- // times代表注册2次或以上 5次或以下的方法
- EasyMock.expect(userDao.getBy("Admin")).times(2, 5);
- // 有时当运行某个方法需要抛出异常时,可以使用andThrow()
- EasyMock.expect(userDao.getAll()).andThrow(
- new NullPointerException("test"));
- }
EasyMock参数匹配器
- @Test
- public void testAdd() {
- IMocksControl mc = EasyMock.createNiceControl();
- userDao = mc.createMock(UserDao.class);
- /*
- * 在使用 Mock 对象进行实际的测试过程中,EasyMock会根据方法名和
- * 参数来匹配一个预期方法的调用.这里就造成了若然实际调用不是输入Ray
- * 就不能成功调用方法的情况,这就需要用到EasyMock中的参数匹配器
- */
- //实际调用时传入任何字符串都可以成功调用,EasyMock.anyString();
- EasyMock.expect(userDao.getBy(EasyMock.anyString()));
- //当传入参数包含R时成功调用
- EasyMock.expect(userDao.getBy(EasyMock.contains("R")));
- //当传入参数满足正则表达式时调用
- EasyMock.expect(userDao.getBy(EasyMock.matches("正则表达式")));
- /*
- * 由于EasyMock提供非常多的参数匹配器,笔者这里就不一一列举了
- * 在实际应用中看情况使用,参数匹配器的名称亦非常有语义,不难掌握
- */
- }
使用EasyMock模拟Servlet容器进行测试
- package com.accentrix.ray;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpSession;
- import org.easymock.EasyMock;
- import org.easymock.IMocksControl;
- import org.junit.Assert;
- import org.junit.Before;
- import org.junit.Test;
- public class TestLoginServlet {
- // 声明要测试的loginServlet和session and request
- private LoginServlet loginServlet;
- private HttpSession session;
- private HttpServletRequest request;
- private IMocksControl mc;
- @Before
- public void setUp() {
- //init mc session and request object
- mc = EasyMock.createStrictControl();
- session = mc.createMock(HttpSession.class);
- request = mc.createMock(HttpServletRequest.class);
- }
- @Test
- public void testSessionIsNull() {
- //注册关系,事件
- EasyMock.expect(request.getSession()).andReturn(null);
- EasyMock.replay(request, session);
- Assert.assertNot(loginServlet.login(request));
- EasyMock.verify(request,session);
- }
- }
总结:
通过使用EasyMock可以有效的在关联或依赖类没提供实现的情况下编写单元测试,而且还能解除单元测试的耦合,可以试想一下,当我们使用EasyMock编写Service层的单元测试时,可以完全不用考虑DAO层的实现,而且成功有效的阻断了对数据库的影响,而DAO层的测试,应该由DAO的测试用例进行测试,从而把Service和DAO的测试完全分离了.EasyMock还可以测试简单的Servlet,但EasyMock也有无力的地方,例如当Servlet跳转(forward/redirect)的时候无法验证到跳转的页面是否正确,又例如在后台将json通过print的方式打印到前台的时候也无法捕获print的值.