jmock是帮助创建mock对象的工具,它基于java开发,在java测试与开发环境中有不可比拟的优势,更重要的是,它大大简化了虚拟对象的使用。本文中,通过一个简单的测试用例来说明jmock如何帮助我们实现这种孤立测试。
我们在测试某类时,由于它要与其他类发生联系,因此往往在测试此类的代码中也将与之联系的类也一起测试了。这种测试,将使被测试的类直接依赖于其他类,一旦其他类发生改变,被测试类也随之被迫改变。更重要的是,这些其他类可能尚未经过测试,因此必须先测试这些类,才能测试被测试类。这种情况下,测试驱动开发成为空谈。而如果其他类中也引用了被测试类,我们到底先测试哪一个类?因此,在测试中,如果我们能将被测试类孤立起来,使其完全不依赖于其他类的具体实现,这样,我们就能做到测试先行,先测试哪个类,就先实现哪个类,而不管与之联系的类是否已经实现。
虚拟对象(mock object)就是为此需要而诞生的。它通过jdk中的反射机制,在运行时动态地创建虚拟对象。在测试代码中,我们可以验证这些虚拟对象是否被正确地调用了,也可以在明确的情况下,让其返回特定的假想值。而一旦有了这些虚拟对象提供的服务,被测试类就可以将虚拟对象作为其他与之联系的真实对象的替身,从而轻松地搭建起一个很完美的测试环境。
jmock是帮助创建mock对象的工具,它基于java开发,在java测试与开发环境中有不可比拟的优势,更重要的是,它大大简化了虚拟对象的使用。
本文中,通过一个简单的测试用例来说明jmock如何帮助我们实现这种孤立测试。有三个主要的类,user,userdao,及userservice。本文中,我们只需测试userservice,准备虚拟userdao。对于user,由于本身仅是一个过于简单的pojo,可以不用测试。但如果你是一个完美主义者,也可以使用jmock的虚拟它。在这领域,jmock几乎无所不能。:)
user是一个pojo,用以在视图中传输数据及映射数据库。其代码如下:
package com.sarkuya.model;
public class user {
private string name;
public user() {
}
public user(string name) {
this.name = name;
}
public string getname() {
return name;
}
public void setname(string name) {
this.name = name;
}
}
userdao负责与数据库打交道,通过数据库保存、获取user的信息。尽管我们可以不用知道jmock如何通过jdk的反射机制来实现孤立测试,但至少应知道,jdk的反射机制要求这些在运行时创建的动态类必须定义接口。在使用jmock的环境中,由于我们要虚拟userdao,意味着userdao必须定义接口。代码如下:
package com.sarkuya.dao;
import com.sarkuya.model.user;
public interface userdao {
public void saveuser(user user);
public user getuser(long id);
}
userservice存有userdao的引用,通过其对外提供应用级的服务。相应地,我们先定义了其接口(尽管在本文中,作为被测试类,userservice不需要有接口,但如果以后此类需要被虚拟,也应该带有接口,基于此原因,我们也为其定义了接口)。
package com.sarkuya.service;
import com.sarkuya.dao.userdao;
import com.sarkuya.model.user;
public interface userservice {
public void setuserdao(userdao userdao);
public void saveuser(user user);
public user getuser(long id);
}
可以看到,除了setuserdao()外,其另外的方法与userdao一样。这是设计模式中门面模式的典型应用,应用只通过userservice提供服务,而userservice在内部通过调用userdao来实现相应的功能。
根据测试先行的原则,你应该先写测试,再编写实现。这里先编写实现的原因,主要是使读者更加清楚我们接着要测试什么。由于本文是着重介绍jmock的使用,加上userserviceimpl比较简单,因此先列出其代码如下:
package com.sarkuya.service.impl;
import com.sarkuya.dao.userdao;
import com.sarkuya.model.user;
import com.sarkuya.service.userservice;
public class userserviceimpl implements userservice {
private userdao userdao;
public userserviceimpl() {
}
public void setuserdao(userdao userdao) {
this.userdao = userdao;
}
public user getuser(long id) {
return userdao.getuser(id);
}
public void saveuser(user user) {
userdao.saveuser(user);
}
}
下面是userservice的测试代码:
package com.sarkuya.service;
import com.sarkuya.dao.userdao;
import com.sarkuya.model.user;
import com.sarkuya.service.impl.userserviceimpl;
import junit.framework.*;
import org.jmock.mock;
import org.jmock.mockobjecttestcase;
public class userservicetest extends mockobjecttestcase {
private userservice userservice = new userserviceimpl();
private mock userdao = null;
public userservicetest(string testname) {
super(testname);
}
protected void setup() throws exception {
userdao = new mock(userdao.class);
userservice.setuserdao((userdao)userdao.proxy());
}
protected void teardown() throws exception {
}
public static test suite() {
testsuite suite = new testsuite(userservicetest.class);
return suite;
}
public void testgetuser() {
user fakeuser = new user("john");
userdao.expects(once()).method("getuser").with(eq(1l)).will(returnvalue(fakeuser));
user user = userservice.getuser(1l);
assertnotnull(user);
assertequals("john", user.getname());
}
public void testsaveuser() {
user fakeuser = new user("john");
userdao.expects(once()).method("getuser").with(eq(1l)).will(returnvalue(fakeuser));
user user = userservice.getuser(1l);
assertequals("john", user.getname());
userdao.expects(once()).method("saveuser").with(same(fakeuser));
user.setname("mike");
userservice.saveuser(user);
userdao.expects(once()).method("getuser").with(eq(1l)).will(returnvalue(user));
user modifieduser = userservice.getuser(1l);
assertequals("mike", user.getname());
}
}
此段代码有几点应注意:
1、此测试类继承了jmock的mockobjecttestcase
2、private mock userdao = null;说明userdao是一个准备虚拟的对象
3、在setup()中,将userdao.class传入mock()后,再通过proxy()方法返回一个userdao的代理类实例(即虚拟对象实例),并赋值于userservice
4、在testgetuser()方法中,如果我们先将第一行及第二行代码屏蔽掉,可以看出,这是一个真实环境下的测试代码。先获取一个user,然后确认其非空值,再确认其姓名为“john”。此时,在真实环境下,这段代码要测试成功的前提必须是userdao已经连接到了数据库,然后返回一个user后传给userservice。
但问题是,到目前为止,且不说userdao还未经历连接数据库这一系列繁琐而痛苦的过程,我们甚至还未实现userdao的接口!那么,为何加上第一行及第二行代码后就可以了呢?这正是jmock的威力所在。先实例化一个测试用的fakeuser,然后通过一系列的指令,在第二行代码中告诉jmock应该如何“做假”。尽管这句代码很长,我们可作如下理解:
1) userdao.expects(once()):我们期望userdao的某方法被执行一次,如果此方法未被执行,或者执行了二次以上,测试就不会通过
2) method("getuser"):这个期望被执行一次的方法名为userdao.getuser()
3) with(eq(1l)):执行getuser()方法时,确认其传入的参数值为“1l”
4) will(returnvalue(fakeuser)):上述条件均满足后,返回一个虚假的对象,即我们前面实例化的fakeuser
总体来说,当设定好第二行语句后,jmock就在后台监控着,确保userdao.getuser()必须,且只被执行一次,且参数“1l”已经正确地传给了此方法,一旦这些条件被满足,就返回fakeuser。
而在第三行,user user = userservice.getuser(1l)将触发所有这些条件,作为奖励,它接受了奖品fakeuser并赋值于user对象。而下面第四行及第五行均对此user对象进行测试,不通过才怪。
5) testsaveuser()方法中的原理类似。其思路是,将id为“1”的user从数据库中取出,将其名改为“mike”,再存回数据库,然后再从数据库中取出此user,确保其名字已被改变。
第五行userdao.expects(once()).method("saveuser").with(same(fakeuser))比较特殊。首先,with(same(fakeuser))说明,传入参数必须是fakeuser此实例,尽管我们在下面的语句中通过user.setname("mike"),但只是改变了其name的属性,而fakeuser的实例引用并未发生改变,因此可以满足条件。其次,其后没有.will(returnvalue(fakeuser)),因为userdao.saveuser()不需要返回任何对象或基本数据类型。
另外,当再次执行userdao.expects()时,jmock将重设其监控条件。我们也可以通过userdao.reset()来显式是清除监控条件。
通过以上实例代码及其说明,我们看出,用好jmock的关键是先设置监控条件,再写相应的测试语句。一旦设好监控条件后,在某段代码块执行完毕时,如果监控条件未得到满足,或是没有通过expects()再次重设条件,或通过reset()来显式是清除监控条件,测试将无法通过。
以上介绍了jmock的基本使用方法。而这种基本用法,占了全面掌握jmock所需学习的知识70%以上。关于jmock的更多细节,感兴趣的读者可以访问jmock的网站进一步学习。