单元测试中使用Mock对象
单元测试中使用Mock对象
一、简单的替换
假设在代码中,你调用你自己的 getTime () 来返回系统当前的日期和时间 :
public long getTime() {
return System.currentTimeMillis();
}
通常建议对应用程序范围外的功能调用进行包装,从而能够更好地封装它们;如上述代码所示,我们是把当前时间的概念包装在自己写的代码里面, 所以调试就容易了一些:
public long getTime() {
if (debug) {
return debug_cur_time;
}else {
return System.currentTimeMillis();
}
}
还可以使用其他的调试路径来操纵 “当前时间” 这个系统概念,从而让那些不这样做而需要等很长时间才能发生的事件马上发生。
只有在代码一致调用你自己的getTime ()、完全没有直接调用Java方法System. currentTimeMillis ()的时候这种方法才有效;但是我们需要的是一种更加干净、更加面向对象化且同时可以实现相同功能的方法。
二、Mock 对象
Mock 对象就是真实对象在调试期的替代品;在许多情况下,mock对象都可以给我们带来帮助:
- 真实对象具有不可确定的行为(产生不可预测的结果, 如股票行情);
- 真实对象很难被创建;
- 真实对象的某些行为很难触发(如网络错误);
- 真实对象令程序的运行速度很慢;
- 真实对象有(或者是)用户界面;
- 测试需要询问真实对象是如何被调用的(例如测试可能需要验证某个回调函数是否被调用);
- 真实对象实际上并不存在(当需要和其他开发小组或者新的硬件系统打交道的时候, 这是一个普遍问题)
借助 Mock 对象,我们就可以解决上面提到的所有问题;在使用 Mock 对象进行测试的时候, 总共有3个关键步骤, 分别是:
- 使用一个接口来描述这个对象;
- 为产品代码实现这个接口;
- 以测试为目的, 在 Mock 对象中实现这个接口.
因为被测试代码只会通过接口来引用对象,所以它完全可以不知道它引用的究竟是真实对象还是 mock 对象。
创建针对真实环境因素的几个对象,其中一个因素就是当前时间:
public interface Envirorunental {
public long getTime();
// ...
}
编写真实的实现代码:
public class SystemEnvironment implements Environmental{
public long getTime() {
return System.currentTimeMillis();
}
//other method
}
Mock的实现:
public class MockSystemEnvirorunent implements Environmental{
public long getTime() {
return current_time;
}
public void setTime(long aTime) {
current_time = aTime;
}
private long current_time;
//...
}
在mock实现里面添加了一个额外的方法 setTime() (以及对应的私有变量), 这让可以控制 Mock 对象;
现在假设编写一个依赖于 getTime() 方法的新方法:Checker.java
public class Checker {
public Checker(Environmental anEnv) {
env = anEnv;
}
public void reminder() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(env. getTime());
int hour = cal.get(Calendar.HOUR_OF_DAY);
if (hour >= 17) { //5:00PM
env.playWavFile("quit_whistle.wav");
}
}
// ...
private Environmental env;
}
在产品环境中, 当初始化这个类的对象时,传入的是一个真实的 SystemEnvironment; 而另一方面, 测试代码传入的则是MockSystemEnvironment;
使用 env. getTime() 的被测试代码并不知道测试环境和真实环境之间的区别, 因为它们都实现了相同的接口;
现在可以借助 Mock对象, 通过把时间设置为已知值, 并检查行为是否如预期那样来编写测试了.
除了已经展示的getTime () , Environmental 接口还有一个 playWavFile() 函数(Checker用到了);通过给 mock 对象添加一些额外的支持代码, 从而能够在不倾听计算机喇叭的情况下, 添加测试来观察它是否被调用了。
public class MockSystemEnvironment implements Environmental{
public long getTime() {
return current_time;
}
public void setTime(long aTime) {
current_time = aTime;
}
private long current_time;
//...
public void playWavFile(String filename){
playedWav = true;
}
public boolean wavwasPlayed(){
return playedWav;
}
public void resetwav() {
playedWav = false;
}
private boolean playedWav = false;
}
整合上方代码:TestChecker.java
public class TestChecker extends TestCase {
public void testQuittingTime() {
MockSystemEnvironment env = new MockSystemEnvironment();
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2004);
cal.set(Calendar.MONTH, 10);
cal.set(Calendar. DAY_OF_MONTH, 1) ;
cal.set(Calendar.HOUR_OF_DAY, 16);
cal.set(Calendar.MINUTE, 55);
long tl = cal.getTimeInMillis();
env.setTime(tl);
Checker checker = new Checker(env);
// Run the checker
checker.reminder();
//Nothing should have been played yet
assertFalse(env.wavwasPlayed());
// Advance the time by 5 minutes
tl += (5 * 60 * 1000);
env.setTime (tl);
//Now run the checker
checker.reminder();
//Should have played now
assertTrue(env.wavwasPlayed());
//Reset the flag so we can try again
env.resetwav();
//Advance the time by 2 hours and check
tl += 2 * 60 * 60 * 1000;
env.setTime(tl);
checker.reminder();
assertTrue(env.wavwasPlayed());
}
}
- 代码创建了一个应用环境的 mock 版本;并设置了我们将使用的假的时间, 然后将它们设置给了mock环境对象;
- 调用 reminder(), 这将(不知情地)使用mock环境。调用断言检查wav文件是不是还没播放, 因为在mock对象的环境中,
此时还不是quitting time; - 但是我们将很快调整时间;把mock时间调整到刚好是 quitting time;然后再一次调用reminder()函数。这次,声音已经被播放过了,因而调用断言确认了.wav文件这次已经播放过了;
- 最后重设 Mock 环境的 .wav 文件的标志, 并且测试再过两个小时之后的情况。
因为已经有了一个提供所有系统功能的现成接口,所以会使用接口而不是直接调用诸如 System.currentTimeMillis() 这样的方法, Mock 对象通过接口拥有了控制一切行为的能力。
三、测试 Servlet
下面的代码列表展示了一个把华氏温标转换为摄氏温标的servlet的部分源代码:
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String str_f = req.getParameter("Fahrenheit");
res.setContentType("text/html");
PrintWriter out = res.getWriter();
try {
int temp_f = Integer.parseInt(str_f);
double temp_c = (temp_f - 32) * 5.0 / 9.0;
out.println("Fahrenheit: " + temp_f + ", Celsius: " + temp_c);
}catch (NumberFormatException e){
out.println( "Invalid temperature: " + str_f);
}
当 servlet 容器接受到请求时, 它自动调用 servlet 的方法doGet(), 并传递给两个参数: request 和 response;
request 参数包含关于请求的信息,servlet使用这个参数来获得华氏温标的值;然后这个温度值被转换为摄氏温标;最后这个结果被发送回给用户。
如果在转换过程中发生了错误(可能用户给温度项中输入的是"boo!" 而不是有效的温度值),捕获这个异常并把错误报告于response 中。
这个代码片断运行于一个相当复杂的环境之中:它需要一个Web服务器和一个servlet容器,并且它需要一个浏览器来与它交互。这几乎没法做自动化的单元测试。Mock对象可以解决这个问题。
servlet代码的接口是相当简单的:它接受两个参数, 一个request和一个response:request对象必须能够在它的getParameter()方法被调用时提供合理的字符串, 而response对象必须能够支持 setContentType () 和 getWriter ()。
具体做法这与在前面的例子中设置假的时间的做法类似:
public class TestTempServlet extends TestCase {
public void test_bad_parameter() throws Exception {
TemperatureServlet s = new TemperatureServlet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.setupAddParameter("Fahrenheit", "boo!");
response.setExpectedContentType("text /html ");
s.doGet(request, response);
response.verify();
assertEquals("Invalid temperature: boo!\ n", response.getOutputStreamContents());
}
public void test_boil() throws Exception {
TemperatureServlet s = new TemperatureServlet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.setupAddParameter("Fahrenheit","212");
response.setExpectedContentType("text/html");
s.doGet(request.response);
response.verify();
assertEquals("Fahrenheit: 212, Celsius: 100.0\ n",response.getOutputStreamContents());
}
}
使用 MockHttpServletRequest 对象来设置要运行测试的上下文;在请求对象中设置参数 Fahrenheit 为值"boo!",这等价于用户在浏览器的相应表单项中填入"boo!"; Mock对象消除了在测试运行时手工输入的必要;
告诉 response对象 期望被测方法设置 response 的 content type为 text/html ;被测方法执行之后,告诉 response 对象验证这是否发生了;Mock 对象消除了人工检查结果的需要;
Mock 对象还能记录给它们传递的数据。在本例中,response对象接收了servlet 需要显示在浏览器上的文本,可以查询这些值来检查返回的文本是否如预期一样。
内容来源-----《单元测试之道–使用Juint》