前言
本文:
1、仅对动态Mock工具和能自动生成Mock Object实现的工具进行比较
2、仅对文中所列的几种有限的工具进行比较
3、仅进行使用方面的对比,不涉及技术方面的比较
可以看作是一份使用Mock Object辅助单元测试的入门手册。
序
在现在流行的各种软件开发的实践或理论中,单元测试(UT,Unit Test)都已经作为一个软件开发的“最佳实践”而被普遍接受。而JUnit则是单元测试工具中无可争议的王者。
当我们对简单对象或算法进行测试时,撰写基于JUnit的单元测试很简单。当我们准备测试一个依赖于其他对象的类或算法时,就需要构造出这些合作者的实例。这时,测试用例的撰写就略显复杂了。更进一步,当我们准备对一个Web系统或复杂的业务进行单元测试的时候,构造“合作者”实例的工作可能就会是低效而昂贵的。
例如:测试一个使用HttpRequest实例的对象,需要启动Web服务器,构造Request实例并填入所需要的值,测试完后需要停止Web服务器。这样,测试数据的构造和环境的准备可能会非常复杂,甚至会难以达到“自动测试”的目标。
Mock Object
此时,Mock Object给我们提出一个解决方案:Mock Object拥有与被测对象的“合作者”完全一致的接口,在测试中作为“合作者”被传递给被测对象;当被测对象调用合作者时,Mock Object根据测试者的意愿改变某些状态或返回期望的结果,以检查被测程序是否按照所期望的逻辑进行工作,达到单元测试的目的。
图1
图2
或者说,Mock Object作为“仿真器”出现在单元测试用例中,对被测对象进行“欺骗”和跟踪。而只要Mock Object的行为与被测对象所期望的一致,就不会对被测对象产生任何影响。
当我们借助Mock Object撰写单元测试用例时,代码样式通常会是这样:
1. 创建:创建Mock Object的实例
2. 训练:设置Mock Object中的状态和期望值
3. 测试:将Mock Object作为参数来调用被测对象
4. 验证:验证Mock Object中的一致性和被测对象的返回值或状态
第2步我们通常称之为Mock Object的训练。此时,我们需要“告诉”Mock Object被测对象会调用它的哪些接口、调用的顺序和次数是什么、它应该做出什么反应。第4步的时候,Mock Object就会按照前面训练的内容一一检查被测对象是否调用了那些指定的接口、调用的顺序和调用次数是否正确。如果正确的话就一切OK,否则说明被测对象没有实现所期望的功能。
从上面的描述中,我们可以看出,Mock Objects
1. 可以仿真被模拟的对象、构建虚拟测试环境;
2. 能够设定有哪些接口将要被调用、并在那时执行期望的动作;
3. 可以验证被测对象是否按照设定的规则调用Mock Object。
Mock Object Generate Tool
最开始,Mock Object是完全由测试者自己手工撰写的。这样,无可避免的会带来编写测试用例效率低下和测试用例编写困难的弊病,甚至可能会影响XP实践者“测试先行”的激情。此时,各种各样帮助创建Mock Object的工具就应运而生了。
这些工具中,有MockObjects、XDoclet等帮助程序员编写Mock Object实现的工具,也有EasyMock、MockCreator等自动创建Mock Object实例/类的工具。本文仅针对后者进行描述。
在这些能自动创建Mock Object实例/类的工具中,又分为两种:一种类似于EasyMock,他们能够动态创建Mock Object实例;一种就像MockCreator,他们可以生成静态的Mock Object代码。最有名且目前仍在维护的有如下几个工具:MockObjects、EasyMock、MockCreator。下面,我们将使用这几种工具分别为下面这个类编写单元测试代码,逐次比较他们使用上的异同:
package mocktest;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SimpleCalcServlet extends HttpServlet
{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
Integer a = Integer.decode(request.getParameter("a"));
Integer b = Integer.decode(request.getParameter("b"));
int result = a.intValue() + b.intValue();
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.print("Result = " + result);
out.flush();
}
}
这个类是一个Servlet,我们可以通过JUnit的一个扩展:HttpUnit来编写它的测试用例。不过,如何使用HttpUnit进行测试不是本文所讨论的内容。
如果利用Mock Object来为这个类编写单元测试用例的话,问题的实质在于如何构造HttpServletRequest和HttpServletResponse的实例。下面,我们将一一展示如何使用上面提到的工具来解决这个问题:
MockObjects
我们有四种方式来利用MockObjects编写测试用例:
1、基于MockObjects的框架编写自己的Mock Object实现并与其他人分享;
2、直接使用MockObjects提供的Mock Object进行测试;
3、使用其中包含的DynaMock模块快速获取Mock Object实例;
4、通过它提供的Helpers对象帮助我们快速构建测试环境。
不过,本文仅关注如何使用DynaMock来编写测试用例[1]:
1. public void testSimpleAdditionUsingDynaMock() throws ServletException, IOException {
2. final Mock mockHttpServletRequest = new Mock(HttpServletRequest.class);
3. final Mock mockHttpServletResponse = new OrderedMock(HttpServletResponse.class,
4. "Response with non-default name");
5.
6. mockHttpServletRequest.expectAndReturn("getParameter", "b", "4");
7. mockHttpServletRequest.expectAndReturn("getParameter", "a", "3");
8.
9. final StringWriter output = new StringWriter();
10. final PrintWriter contentWriter = new PrintWriter(output);
11.
12. mockHttpServletResponse.expect("setContentType", "text/html");
13. mockHttpServletResponse.expectAndReturn("getWriter", contentWriter);
14.
15. SimpleCalcServlet aServlet = new SimpleCalcServlet();
16. aServlet.doGet((HttpServletRequest) mockHttpServletRequest.proxy(),
17. (HttpServletResponse) mockHttpServletResponse.proxy());
18.
19. mockHttpServletRequest.verify();
20. mockHttpServletResponse.verify();
21. assertEquals("Output should be an addition", "Result = 7", output.toString());
22. }
对照“Mock Object”一节所言的“代码样式”,Line2~4即为“创建”Mock Object实例:
●我们使用了两个不同的类来构造Mock Object:一个是Mock,一个是OrderedMock。他俩的异同在于一个不检测接口被调用的次序,一个需要检测接口被调用的次序。
Line6~13为“训练”Mock Object:
●在训练Mock Object的时候,我们调用了它的expectAndReturn()方法。此方法有多个重载。对于测试方法第6行来说,它表达了这样一个意思:被测代码将传入参数“b”来调用mockHttpServletRequest的getParameter()方法;当接收到此消息时,mockHttpServletRequest将返回字符串“4”。
Line15~17为“测试”:
●实际进行测试的时候,通过调用Mock类实例的proxy()方法将Mock Object实例传递给被测代码。
Line19~21为“验证”:
●此时,使用Mock类实例的verify()方法进行一致性验证。
[1] 为了节省篇幅、简单起见,本文只列出JUnit测试用例中test方法的代码。