单元测试中使用Mock对象

本文介绍了在单元测试中如何使用Mock对象,包括简单替换、Mock对象的使用及其在Servlet测试中的应用。通过Mock对象,可以更好地控制测试环境,避免依赖真实环境,提高测试的效率和准确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单元测试中使用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个关键步骤, 分别是:

  1. 使用一个接口来描述这个对象;
  2. 为产品代码实现这个接口;
  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》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值