Mock不是测试的银弹

发表于:2009-5-19 10:34

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:胡凯    来源:InfoQ

分享:

  测试中的stdout是在真实环境下运行Perforce命令行所采集的标准输出(stdout)样本, 通过mock perforce对象,我们可以轻易的控制changes方法的返回值,让验证解析逻辑的正确性变得非常容易,采用mock技术使开发者无需顾忌 Perforce服务器的存在与否,而且可以采用不同的stdout来覆盖不同的情况。然而危机就在这看似完美的测试过程中被埋下了,事实上 Perforce stdout中的时间格式会依用户环境的设定而变化,从而进一步导致parseChanges方法中的解析逻辑出现异常。由于测试中的stdout全由假设得来,并不会依照环境变化,即便我们将测试跑在多种不同的环境中也没能发现问题,最终在产品环境才由客户发现并报告了这个缺陷。

  真实perforce对象的行为与测试所使用的mock对象行为不一致是出现上述问题的根本原因,被模拟对象的行为与真实对象的行为必须完全一致称之为mock对象的行为依赖风险。开发者对API的了解不够、被模拟对象的行为发生变化(重构、添加新功能等修改等都可能引起被被模拟对象的行为变化)都可能导致错误假设(与真实对象行为不一致),错误假设会悄无声息的引入缺陷并留下非法测试。非法测试在这里所代表的含义是,它看起来很像测试,它运行起来很像测试,它几乎没有价值,它几乎不会失败。在开发中,规避行为依赖风险最常见的方法是编写功能测试,由于在进行mock测试时,开发者在层与层之间不断做出假设,而端到端的功能测试由于贯穿了所有层,可以验证开发者是否做出了正确的假设,然而由于功能测试编写复杂、运行速度慢、维护难度高,大部分产品的功能测试都非常有限。那些通过 mock测试的逻辑,便如埋下的一颗颗定时炸弹,如何能叫人安心的发布产品呢?

  《UNIX编程艺术》中有一句话“先求运行,再求正确,最后求快”,正确运行的测试是高质量、可以快速运行测试的基础,离开了正确性,速度和隔离性都是无根之木,无源之水。那么采用真实环境就意味着必须承受脆弱而缓慢的测试么?经历了一段时间的摸索,这个问题的答案渐渐清晰起来了,真实环境的测试之所以痛苦,很大程度上是由于我们在多进程、多线程的环境下对编写测试没有经验,不了解如何合理的使用资源(所谓的资源可能是文件、数据库中的记录、也可能是一个新的进程等),对于我们,mock测试作为“银弹”的作用更多的体现在通过屏蔽运行在单独进程或者线程中的资源,将测试简化为对大脑友好的单线程运行环境。在修复过足够多的脆弱测试后,我们发现了编写健壮测试的秘密:

  要设计合理的等待策略来保守的使用外部系统。很多情况下,外部系统处于某种特定的状态是测试得以通过的条件,譬如HTTP服务必须启动完毕,某个文件必须存在等。在编写测试时,开发者常常对外部系统的估计过于乐观,认为外部系统可以迅速处于就绪状态,而运行时由于机器和环境的差异,结果往往不如开发者所愿,为了确保测试的稳定性,一定要设计合理的等待策略保证外部系统处于所需状态,之所以使用"等待策略"这个词,是因为最常见”保证外部系统处于所需状态“的方法是万恶的"Thread.sleep", 当测试运行在运算速度/网络连接速度差异较大的机器上时,它会引起随机失败。而比较合理的方法是利用轮询的方式查看外部系统是否处于所需状态(譬如某个文件存在、端口打开等),只有当状态满足时,才运行测试或者进行Assertion,为了避免进入无限等待的状态,还应该设计合理的timeout策略,帮助确定测试失败的原因。

  要正确的创建和销毁资源漠视测试环境的清理也常常是产生脆弱测试的原因,它主要表现在测试之间互相影响,测试只有按照某种顺序运行时才会成功/失败,这种问题一旦出现会变的非常棘手,开发者必须逐一对有嫌疑的测试运行并分析。因此,有必要在开始时就处理好资源的创建和销毁,使用资源时应当本着这样一个原则:谁创建,谁销毁。 junit在环境清理方面所提供的支持有它的局限性,下面的代码是使用资源最普遍的方式:

  @After
public void teardown() {
   //销毁资源A
   //销毁资源B
}

@Test
public void test1() {
   //创建资源A
}

@Test
public void test2() {
   //创建资源B
}

  这个框架可以更好的规范资源的创建和销毁的过程,减少因为测试环境可能引起的随机失败,当然这个框架也有其局限性,在ResourceIsCreated 和ServiceIsStarted之间共享状态会比较复杂,在我们的产品中,Precondition大多用于启动新进程,对于共享状态的要求比较低,这样一套机制就非常适合。每个项目都有其特殊性,面对的困难和解决方案也不尽相同,但在使用资源时如果能遵守“谁创建,谁销毁”的原则,将会大大减小测试之间的依赖性,减少脆弱的测试。

  为了确保资源A与资源B被正确销毁,开发者必须将销毁资源的逻辑写在teardown方法中,然而运行用例test1时,资源B并未被创建,所以必须在 teardown中同时处理资源A或B没有被创建的情况,由于需要销毁的资源是用例中所使用资源的并集,teardown方法会快速得膨胀。由于这样的原因,我在开源项目junit-ext中加入了对Precondition的支持,在测试用例运行前,其利用标注所声明的多个Precondition的setup方法会被逐一调用来创建资源,而测试结束时则调用teardown方法销毁资源。

  @Preconditions({ResourceIsCreated.class, ServiceIsStarted.class})
@Test
public void test1() {
      //在测试中使用资源
}

public class ResourceIsCreated implements Precondition {
    public void setup() {
           //创建资源
    }
    public void teardown() {
           //回收资源
    }
}

public class ServiceIsStarted implements Precondition {
     public void setup() {
           //创建资源
     }
     public void teardown() {
           //回收资源
     }
}

public interface Precondition {
     void setup();

     void teardown();
}

32/3<123>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号