用Visual Studio实践敏捷测试(四) 上

     在上一篇中我们介绍了如何编写自动化的测试用例,在拥有了一定数量的自动化测试之后,随之而来一个很自然的问题就是如何有效地利用这些测试更好地在敏捷开发的过程中保证产品的质量。在这一篇中我们就来讨论一下基于不同目的的各种生成(Build)和测试运行(Test Run)以及如何实现这些运行。

 

封闭签入(Gated Check-in)

     在本文的第一篇中我们曾经提到过在敏捷软件开发过程中每一个Sprint结束时团队都应该有一个可以运行和演示的版本。这对团队成员来说是一个艰巨的任务。因为一方面必须在有限的时间内完成用户故事的开发,提供相对完整的用户体验,这就要求开发人员“敏捷”迅速的签入代码;而从另一个角度来说,频繁而迅速的签入显然会对生成的稳定性带来极大的挑战。尤其是当团队成员以用户故事而不是产品部件来分工时,签入与签入之间常常会有重叠的部分。在这种情况下,如何才能保证各项签入在集成到产品中后,产品还能正常工作呢?封闭签入就是一种比较严格的实时保护产品功能的方法。

 

     所谓封闭签入是指在签入代码之前,先尝试生成文件,如果生成失败则拒绝签入,只有在生成成功的情况下代码才会被签入代码控制系统中去。这里的生成,可以是单纯的编译生成,也可以在此基础上再运行一些指定的自动化测试以验证生成的质量。使用这种方法的优点是能很好地确保了指定的用户场景(即指定的测试用例集所覆盖的用户场景)不会被破坏,而缺点就是拖延了时间甚至可能造成阻塞——签入必须等待生成完成、测试用例运行结束,而且签入还可能被拒绝,一个签入可能要反复多次提交,更糟糕的是在几次提交之间其他团队成员可能也提交了签入请求,这又进一步延长了一次签入所需的时间。

 

     为了使封闭签入不至于成为开发流程中的瓶颈,我们必须在生成测试覆盖和生成时间二者之间找到一个平衡点,即在保证每次签入能在较短的时间内(通常我们认为<10分钟是比较理想的范围,10~15分钟是可以接受的范围)将结果返回的前提条件下选择适当的测试用例以达到所期待的用户场景覆盖。很显然,为了满足上述要求,测试用例的运行时间及其覆盖的内容是选择测试的主要依据,而另外一条不可忽视的选择标准是测试的稳定性——运行不稳定的测试用例显然不适合用于在短时间内决定签入是否符合要求。以下列出几条我们选择封闭签入测试集的原则供大家参考:

  • 从基础用户场景入手:我们不可能在封闭签入中保证所有已有的功能不被破坏,在时间有限的情况下,只能丢卒保车,只关注那些基础的、核心的用户场景,围绕这些场景选择测试。对于其他功能场景的验证保护将由我们稍后介绍的其他一些测试运行来完成。
  • 单元测试是封闭签入测试集的主力:开发人员编写的单元测试通常是直接与产品部件接口对话,运行稳定且速度快,是封闭测试的首选。特别是在我们的开发团队中,对于开发人员签入的功能严格规定了必须同时签入相应的单元测试,这使得单元测试对产品功能的覆盖率相当高,使其更能胜任封闭签入测试的职责。
  • 在单元测试基础上补充部分验收测试:单元测试虽然以运行速度快且稳定两大优势占据了封闭签入测试的主要部分,但是单元测试所针对的还是独立的产品部件或是方法,保证产品各部件正常运作不代表各部件集成起来之后仍正常工作。所以为了确保产品的端到端用户体验,我们有必要从测试人员编写的验收测试中抽取部分核心场景的测试来完善整体用户场景的测试覆盖。
  • 尽量不要在封闭签入测试集中引入UI测试:UI测试不可避免的与运行速度慢和不稳定联系在一起,与封闭签入的需求几乎正好相反。但我们也不能一概而论,直接规定不允许在封闭签入测试集中添加UI测试。比如特定的UI测试能提供别的测试无法达到的产品覆盖,且该测试覆盖到的用户场景十分重要,在这种情况下也可以考虑将该测试添加进测试集,当然前提条件是,该测试足够稳定且运行时间不能过长。

     在我们的实践过程中,基本上就是采用了单元测试+核心非UI验收测试这样的组合。另外,我们还在部署测试运行方面利用了Visual Studio提供的测试代理(Test Agent)功能实现测试的分布式运行,大大缓解了运行时间和测试覆盖之间的矛盾,为更全面的用户场景覆盖提供了可能。我将在本文稍后对测试代理再做介绍。

 

滚动生成(Rolling Build)

     滚动生成是另一种实时的保护产品已有功能的方法。与封闭签入拦截签入并插入验证的做法不同,滚动生成并不监控代码的签入,而是采取了相对宽松的方式——允许随时直接签入代码,在代码签入之后再进行相应的验证。滚动生成自动对当前时刻已签入的产品代码重复执行生成文件和运行指定测试操作,每一次的滚动事实上验证的对象是从上一次滚动开始时刻到当前时刻之间的所有签入。当某一次的滚动生成失败时,在此期间签入代码的团队成员应该负责尽快找出并修复错误,或者在错误无法迅速修复的情况下撤回导致问题的签入。也就是说滚动生成并不会直接影响团队成员的正常开发工作,但是要求大家在确实出现问题时暂停手头的其他工作及时将产品代码恢复至一个合理的状态。

 

     对滚动生成测试集的选择标准相对于封闭测试要宽松一些,因为滚动生成并不要求签入的个人等待其结果,所以没有太大的运行时间压力。我们对滚动生成的期望一般是1个小时左右滚动一次。在实践中我们囊括了几乎所有验收测试,对各项功能都有一定程度的覆盖,在滚动生成保持通过的状态的情况下,我们对产品代码的实时质量还是有相当的信心的。另外,为了规避不稳定的测试用例带来的不必要的结果分析时间,我们还引入了再次运行失败的测试的策略,只有2次运行都失败的情况下,滚动生成的结果才会被认为是失败。

 

     与封闭签入相比,滚动生成的主要优势包括:

  • 滚动生成不阻碍签入,不影响正常的开发节奏,不会造成瓶颈
  • 滚动生成每一次运行针对多个签入,测试的效率更高
  • 滚动生成不要求很短时间内出结果,换句话说,可以允许执行更多的测试用例

 

     而滚动生成也有其不足之处:

  • 滚动生成并不能保证产品“随时”都处在可正确运行基本功能的状态
  • 滚动生成不能及时将签入的质量反馈给团队,例如在某一次滚动刚开始后发生的签入几乎需要2次滚动的时间才能确认是否达到了签入的要求
  • 滚动生成在出错时需要分析多个签入以找出问题所在,花费了额外的时间

 

     在实践过程中,通常封闭签入或是滚动生成中的一个就可以满足团队对于产品功能保护的需要了,开发团队可以根据实际情况选择其中之一。不过有时也可以将二者结合起来同时使用。同时使用的好处是分工更加明确,封闭签入就只运行单元测试等少量最核心的测试,甚至可以不运行测试只生成文件,尽可能减少额外的等待时间;而滚动生成则负责完成对基本用户场景的完整覆盖。

 

定期测试运行

     前面介绍的两种生成+测试运行都是用于功能的实时保护的,都对运行时间上有较严格的要求,所以测试用例中大量的功能测试通常是不会在这些测试运行中被执行的。完整的测试集通常只在定期测试运行中使用。这里的定期可以是每天一次、每周一次或是两周一次等等,可以根据团队具体需要以及完整测试集运行所需的时间来确定。

 

     我们的产品开发使用的是三层的分支结构的源代码树,平时的改动都在功能分支(Feature Branch)上进行,每一个Sprint或一个里程碑做一次和产品单元分支(Product Unit Branch)的集成,从产品单元分支到产品主干(Main Branch)的集成间隔时间则会更长一些。不同子分支到父分支的集成交错进行,以避免冲突。我们针对每一个层级的特点采用了不同频率的定期测试运行,比如:

  • 功能分支上做每日(夜)测试运行,保证一天的改动在晚上经过验证
  • 产品单元分支上做每周1~3次测试运行,来验证产品单元内各功能集成的情况
  • 产品主干上做每周或两周一次的测试运行,监控产品整体质量

 

     测试运行的频率还可以根据产品开发周期的不同阶段做相应的调整,比如刚开始在大量新功能的开发阶段时,还没有集成的用户体验,可以侧重于功能分支和产品单元上的测试运行;而在接近产品发布阶段时,测试运行的重点就应该是主干上的整体用户体验了。

 

其他测试运行

 

     上述测试运行都是针对产品功能的运行,在开发过程中我们还会进行一些有特殊针对性的测试运行,比如性能测试运行、压力测试运行、本地化测试。这些测试运行一般是按需进行的,不强行规定运行的频率。

另一类测试运行是针对测试本身的代码覆盖率测试运行。这种测试运行的目的是统计已有的测试集对产品代码的覆盖率,并通过分析未被覆盖到的代码发现测试的漏洞,进一步完善测试。这类测试运行的频率随产品代码改动量而定,改动越多越频繁,测试运行也应该越频繁,以保证足够的代码覆盖。

 

 

林俊彦

软件测试开发工程师

 

本文收录于《程序员》10月刊。