但是在一个运行环境比较复杂的应用程序的UT阶段,为了要测试目标应用程序,我们往往需要花费很多时间去构筑一个包含诸如Application Server, Message Server, Database Server等服务的可运行环境。这对于系统ITa或者ITb而言并不是什么问题,但是很多时候对于UT来说却是一项不必要的工作。(在很多Global性质的开发项目中,每一个location都构筑这样一个环境更是一项巨大的开销)
Mock Ojbect的出现很好的解决了这一难题。例如,你写了一个封装了JMS API包的补助类来提供对Message Server的访问。示例代码如下:
public class JmsAgent extends JmsAgentBase implements JmsAgentIF {
private Connection connection;
public void sendMessage(Session session, Destination destination,
Object object) throws DomainException {
MessageProducer producer = null;
producer = session.createProducer(destination);
//performing send action within a loop in case of the message content object is an array
Object[] msgContents = new Object[] {};
if (object instanceof Object[]) {
msgContents = (Object[]) object;
} else {
msgContents[0] = object;
}
Message message = null ;
for (int i = 0; i < msgContents.length; i++) {
message = msgCreator.createMessage(msgContents[i],
session);
if (logger.isDebugEnabled() == true) {
logger.debug("Message[" + message.getJMSMessageID()
+ "] is to be sent to Destination["
+ destination.toString() + "].");
}
producer.send(message);
}
//transaction management processing
if(this.isMethodTransacted() == true){
session.commit();
}
public Object receiveMessage(Session session, Destination destination)
throws DomainException {
Object result = null ;
if (logger.isDebugEnabled() == true) {
logger.debug("Receive Message from Destination["
+ destination.toString() + "].");
}
MessageConsumer consumer = null;
consumer = session.createConsumer(destination);
result = getMsgVisitor().visit(consumer.receive(receiveTimeout));
//transaction management processing
if(this.isMethodTransacted() == true){
session.commit();
}
return result ;
}
public void setConnection(Connection connection){
this.connection = connection;
}
省略…
}
当要对你的程序逻辑代码进行单体测试的时候,我们并不关心JMS API的实现部分,而是将重点放在应用程序的逻辑代码块的准确性上。但是为了让应用程序能够执行起来,我们必须保证程序所调用的JMS API可以正常使用,这就需要有一个JMS API服务商提供的实装包,以及响应该实装的Message服务。(可以是Active MQ, WebSphere MQ 或者其他服务商所提供的)。在系统配置比较复杂的情况时,比如说需要Cluster构成,需要配置很多Channel, Queue等等,往往配置系统的时间要比我们做UT的时间花费更加大。
这个时候我们会想到Mock Object来模拟JMS API实现以及相应的Message服务。
我们最容易想到的方式就是,对上述补助类里使用了的每一个JMS API,我们都为其生成一个Mock Ojbect,并模拟相应方法的实现。在下面的示例代码中,我们就采用了这种方式,并且在Message服务的模拟实现中,我们将用一个类的属性LinkedList来保存和获取Message。
public class JmsAgentTest extends TestCase {
private LinkedList messageContainer = new LinkedList();
private Object[] testMsg = new String[]{"hello world_1!","hello world_2!"};
private JmsAgent jmsAgent ;
protected void setUp() throws Exception {
jmsAgent = new JmsAgent();
jmsAgent.setConnection(new MockConnection());
}
public void testSendMessage() throws Exception {
jmsAgent.sendMessage(jmsAgent.getSession(),new MockDestination(),testMsg);
}
public void testReceiveMessage() throws Exception {
Object mse = null ;
for(int i=0;i<testMsg.length;i++){
msg = jmsAgent.receiveMessage(jmsAgent.getSession(),new MockDestination());
asertNotNull(msg);
System.out.println(msg.toString());
}
}
class MockConnection implements Connection {
public Session createSession(boolean arg0, int arg1) throws JMSException {
return new MockSession();
}
省略…
}
class MockSession implements Session{
public TextMessage createTextMessage(String arg0) throws JMSException {
return new MockTextMessage(arg0);
}
public MessageProducer createProducer(Destination arg0) throws JMSException {
return new MockProducer();
}
public MessageConsumer createConsumer(Destination arg0) throws JMSException {
return new MockConsumer();
}
省略…
}
class MockTextMessage implements TextMessage{
String text;
public MockTextMessage(String text){
this.text = text;
}
public String getText() throws JMSException {
return this.text;
}
省略…
}
class MockProducer implements MessageProducer{
public void send(Message arg0) throws JMSException {
messageContainer.add(arg0);
}
省略…
}
class MockConsumer implements MessageConsumer{
public Message receive() throws JMSException {
MockTextMessage message = messageContainer.getFirst();
messageContainer.removeFirst();
return message;
}
省略…
}
class MockDestination implements Destination{
省略…
}
}
在这个例子里,我们可以看到使用Mock Object手法,即使没有JMS API实装包及其提供Message服务,我们也能够很好的测试该补助类的逻辑代码。但是同时我们也可以看到,大量的Mock Object的实现也给我们带来了不必要的麻烦,即使是很单纯的模拟内容,也需要我们实现大量的相关类和接口,不仅增加了我们需要维护的代码量,而且在对应变更或者对代码进行重构时也会增加很多工作量。
为了解决这个问题,我们引入的是Mock Object的生成工具这一利器。
在Java中,动态代理的实现已经是非常的普及了,先是由JDK1.3.1开始提供了针对目标接口的动态代理机制,随后由开源项目CGLib提供了针对目标类的动态代理机制,两者形成互补,已经在很多其他的开源项目中得到了广泛运用。同样,从我们上面的例子也可以看得出来,在Mock Object的生成上动态代理机制也可以起到很好的作用。利用东带代理机制来协助生成Mock Object的开源项目中,EasyMock和JMock是比较出名的两个。不管是在实现上,还是在提供的功能上两者都很相似。JMock在方法参数测试的灵活性有更大的优势,并且在使用时的写法上,对于Java程序员来说更具有亲和力,因此在接下来的内容中我会简单介绍一下JMock的使用方法。
JMock的官方网站是http://www.jmock.org, 可以从那里下载到最新的程序源代码以及编译后的jar包,上面还有一些使用说明以及实例可以参照。
1.首先我们要知道JMock可以为我们做什么。大体上可以分为下面两点:
1-1.通过动态代理机制为对象接口或者对象类的生成Mock Object
1-2.在测试过程中控制与检查生成的Mock Object的行为,例如可以事先指定什么方法会被调用多少次,什么参数会被传递,被调用时执行什么逻辑,调用的顺序等等。当实际运行结果与事先指定的内容有出入时JMock会抛出例外并结束测试过程。
2.其次要了解的是JMock中的几个概念。
1) Matcher: Macher是用来制定测试测试前后的调用结果之间的匹配规则的一组类。主要有下面两种用法。
1)-1。用户直接使用InvocationMatcher来制定调用时的匹配规则,比如目标方法会被调用1次,或是至少/至多1次,指定的n次等内容。例如:
mockSession.expects(once()).method("createTextMessage")
就表示在测试过程中Session对象的createTextMessage方法要求被调用一次。
1)-2。在运行期,JMock内部使用了InvocationMatcher子类,基于用户事先指定好的各种条件判断运行前后的调用结果是否匹配。如ArgumentsMatcher,
MethodNameMatcher等。
2) Constraints: 在JMock中,Constraint是用来限制,或是指定传递到某些方法的参数类型时使用到的一组类。此外也可以用于限制,或是指定目标方法。
例1.在如下的代码中就使用了IsInstanceOf这一constraint。
mockSession.expects(once()).method("createTextMessage").
with(new IsInstanceOf (String.class));
在调用Session实例的createTextMessage方法时限制传递给该方法的参数必须是String类型的。
例2.除了限制方法参数之外,在指定目标方法时,constraint也可以起到一定作用。
mockSession.expects(once()).method(IsAnything).withAnyArgument().will(xxx);
这行代码中用到了IsAnything这个constraint,表示在测试过程中要求mockSession对象的某个方法必须要被调用一次,而且被调用时会执行xxx指定的逻辑。这一用法在下面谈到Stub的用法时会有所帮助。
3) Stub: 在JMock中,Stub和下面的Expectation具有相同意义。都是用来事先设置Mock Object在测试过程中会被调用的方法。不同之处在于用Stub设置的方法在测试过程中允许被调用0次或者多次,而用Expectation设置的方法则不同,需要在设置时使用InvocationMatcher实例指定方法会被调用的次数。也就是说有两者在使用方式上存在不同。
此外,Stub的另外一个重要的用法是作为一种处理被设置到目标方法上,在运行期间该方法被调用时会自动执行Stub内的处理。在4)我们会详细说明这个用法。
例1.下面的例子指定了Connection类的start方法可以在测试过程中被任意的调用。
mockConnection.stubs().method("start").withNoArguments();
例2.有很多时候,在应用程序大量使用了某个对象的各式各样的方法,我们并不想知道它的什么方法被调用过,以及被调用过多少次,对于这样的对象的Mock Object,在测试过程中可以用这种方式设置:
mockSession.stubs().method(IsAnything).withAnyArguments();
4) Expectation: 关于Expectation,在上面的Stub的内容中已经有所描述,这里举例补充一下对于目标方法,我们可以设置什么处理。
例1.不设置,设置返回值,设定抛出例外等简单处理。
mockSession.expects(once()).method(“createTextMessage”).with (new IsInstanceOf (String.class).will(new ReturnStub((TextMessage)mockTextMessage.proxy())));
在这个例子中,Session对象的createTextMessage方法被调用时将会返回一个TextMessage对象的MockObject.其中的Stub: ReturnStub是由JMock自己实现的。
例2.当某个方法会被多次调用时,设置对每次调用时的处理按照某个顺序执行;
mock.expects(atLeastOnce()).method(m).with(...)
.will( onConsecutiveCalls(
returnValue(10),
returnValue(20),
throwException(new IOException("end of stream")) ) );
这个例子是JMock官方网站上提供的,当方法m被第一次调用时,它会返回一个指定值10,同样的,第二次被调用时会返回20,第三次时会抛出指定的例外。
例3.设置定制化的处理。首先需要实现这么一个定制化处理,然后像上面的例子那样将其指定为Mock Object的目标方法的处理即可。
首先实现一个Stub类,
private class HelloWorldStub implements Stub {
public Object invoke(Invocation invocation) throws Throwable {
return new MockTextMessage(“HelloWorld”);
}
public StringBuffer describeTo(StringBuffer buffer) {
return buffer.append("returns ").append(“HelloWorld”);
}
}
使用该Stub类
mockSession.expects(once()).method(“createTextMessage”).with (new IsInstanceOf (String.class).will(new HelloWorldStub());
例4.限制相同或者不同Mock Object之间调用的先后顺序
MockConnection.expects(once()).method(“start”).withAnyArguments().id(“startC”);
MockConsumer.expects(once()).method(“receive”).withAnyArguments().after(“startC”).will(new HelloWorldStub());
MockConnection.expects(once()).method(“stop”).withAnyArguments().after(“startC”);
3.最后,回到我们最开始的那个例子中,现在我们已经对JMock的使用有了一定的了解,下面我们要用JMock来改善一下这个测试代码。
注: MockObjectTestCase是JMock实现的TestCase的一个子类,
其中提供了很多方便的可以获取现成的Stub,Constraint,Mather类的方法。并且,使用该类提供的方法来获取目标接口或目标类的Mock Object的话,不需要在测试方法用调用相应的MockControl实例的verify()方法即可将设置好的内容反映到测试过程中。
public class JmsAgentTest extends MockObjectTestCase {
private LinkedList messageContainer = new LinkedList();
private Object[] testMsg = new String[]{"hello world_1!","hello world_2!"};
private JmsAgent jmsAgent ;
Mock mockCon ;
Mock mockSession ;
Mock mockTextMessage ;
Mock mockProducer ;
Mock mockConsumer ;
Mock mockDestination ;
protected void setUp() throws Exception {
jmsAgent = new JmsAgent();
mockCon = mock(Connection.class);
mockSession = mock(Session.class);
mockTextMessage = mock(TextMessage.class);
mockProducer = mock(MessageProducer.class);
mockConsumer = mock(MessageConsumer.class);
mockDestination = mock(Destination.class);
mockCon.stubs().method(new IsAnything()).withAnyArguments();
mockSession.expects(atLeastOnce()).method("createTextMessage").with(isA(String.class)).will(returnValue((TextMessage)mockTextMessage.proxy()));
mockSession.stubs().method("commit").withNoArguments();
mockSession.stubs().method("rollback").withNoArguments();
mockSession.stubs().method("close").withNoArguments();
省略….
jmsAgent.setConnection(mockCon.proxy());
}
public void testSendMessage() throws Exception {
mockProducer.expects(exactly(testMsg.length)).method("send").with(isA(Message.class)).will(new ActionStub());
mockTextMessage.expects(exactly(testMsg.length)).method("setText").
with(isA(String.class)).will(new MessageStub());
省略…
jmsAgent.sendMessage(jmsAgent.getSession(),mockDestination.proxy(),testMsg);
}
public void testReceiveMessage() throws Exception {
mockConsumer.expects(exactly(testMsg.length)).method("receive").
with(ANYTHING).will(new ActionStub());
mockTextMessage.expects(exactly(testMsg.length)).method("getText").withNoArguments().will(new MessageStub());
省略
Object mse = null ;
for(int i=0;i<testMsg.length;i++){
msg = jmsAgent.receiveMessage(jmsAgent.getSession(),mockDestination.proxy());
asertNotNull(msg);
System.out.println(msg.toString());
}
}
private class ActionStub implements Stub {
private String strSendMethod = "send";
private String strReceiveMethod = "receive";
public Object invoke(Invocation invocation) throws Throwable {
//Class className = invocation.invokedObject.getClass();
String methodName = invocation.invokedMethod.getName();
if(methodName.startsWith(strSendMethod)){
messageContainer.add((TextMessage)invocation.parameterValues.get(0));
return null;
}else if(methodName.startsWith(strReceiveMethod)){
//If return the MockTextMessage instances set by sendMessage method,
//the expected setting like setText will be effective.
//TextMessage msg = (TextMessage) messageContainer.getLast();
//messageContainer.removeLast();
return mockTextMessage.proxy();
}else {
return null;
}
}
public StringBuffer describeTo(StringBuffer buffer) {
return null;
}
}
private class MessageStub implements Stub {
private int messageBodyIndex ;
private String strSetMethod = "set";
//private String strGetMethod = "get";
public MessageStub(int messageBodyIndex) {
this.messageBodyIndex = messageBodyIndex;
}
public MessageStub() {
}
public Object invoke(Invocation invocation) throws Throwable {
String methodName = invocation.invokedMethod.getName();
if(methodName.startsWith(strSetMethod)){
messageBodyContainer.add(invocation.parameterValues.get(0));
return null;
}else {
String messageBody = (String) messageBodyContainer.getFirst();
messageBodyContainer.removeFirst();
return messageBody;
}
}
public StringBuffer describeTo(StringBuffer buffer) {
return null;
}
}
}
如果需要详细代码的话,请到下面连接处下载相应的压缩文件。
http://comefos.sourceforge.net/articles/index_articles.html