chapter 6自问自答

1. 49/33页。为什么作者一方面谈论Automation strategy(自动化策略),一方面又说不要做“big design upfront"(不要预先做大量设计,BDUF)?

    这并非是个测试问题,更应该是开发问题。从开发方法学来说,正对应着目前流行的开发方法的两个极端。相对传统的瀑布模式,以及集大成的CMMI,更注重预先做足够的设计。RUP的偏重流程的分支,也是鼓励这种方式。我记得刚工作时参加过一个Rational Rose的分享会,主讲人就介绍他们把开发人员分为设计人员和编码人员。设计人员就负责画UML图,然后通过正向工程自动生成代码。然后编码人员再对这些代码做修改和填充。当时还觉得他们很强大,但现在很怀疑他是不是真的这么做了。而相对较晚,其实已经不能算年轻的敏捷开发,则站在另外一端。其强调可工作的代码胜过文档。因为意识到需求可能随时发生变化,所以或多或少放弃了前期的设计,而直接进入编码实践,在实践中不断的重构以应对变化。RUP的正统的分支(我认为正统的),则是作了一些折中。它强调架构骨架。它会做设计,但只关注影响架构的设计。以前看温昱老师的书,觉得他对如何界定设计的度讲的挺好的,可惜现在都忘光了。
   其实所有实践中的方法都不在两个极端。最瀑布的也会在实际中不断修改代码,进而影像设计;最敏捷的也会在草纸上画设计图,或者口头会议来讨论设计。就我而言,对于我所擅长的,或者相对有把握的(技术层面),可能不会做任何设计(以及可行性分析,原型系统等);但如果是初次接触,或者毫无把握,则会做相对详细的设计文档。换用老孟的话就是,”高手是不需要写文档的。“(记不清是他本人说的还是我总结的了)。CMMI的问题就在于把所有人,无论高手低手,都看做低手。(这让我想到《万历十五年》对传统中国管理的批评。)每个人都成了缓慢流程的一个可有可无的分子,整个流程的速度依赖于最慢的那个分子。而高效的人,只能够花大量的时间来应付可有可无的”设计文档“。相反,敏捷因为没有流程的限制,更依赖于人的素质。一个团队中如果有几个有团队精神的技术高手,那哪怕采用最极端的敏捷,结果也说得过去。而且我们可以通过反馈不断的调整啊。
    回到这个测试问题。我曾经因为采用错误的自动化策略而做了大量的”无用功“。因为采用了不稳定的工具(现在可能好多了,现在很多人都认为该工具是王道)和错误的测试策略(完全依赖终端测试而非分层的测试),测试效果很不好。这固然是采用错误的自动化策略导致的,但这种错误是否可以被”big desing upfront“给解决呢?我觉的不够稳定的测试工具和巨大的测试维护量,即使预先做大量的设计,工具调查,原型系统,也未必能在实验环境下发现。更好的解决方法是在实际工作中,当发现效果不如人意时,再分析问题,解决问题。事实上这正是这么做的,只不过可以做的更早一些。
那既然如此,自动化策略又有什么用呢?完全可以通过实践中的”反馈-重构“循环来解决问题啊。我想这里的自动化策略是无数先烈的经验总结。它未必一定正确,但可以帮助少走弯路。正如不写文档的高手,不正是一系列自己和他人的经验才成就了自己的不写文档的底气吗?当然,不管是别人的或自己的经验,还是要不断的在实践中根据反馈不断重构。
    这就是我对BDUF的理解,应该也是作者的意思。

2. 50/34页 Per-functionality test和Cross-functionality test的区别

    不知道这两个术语是公认术语还是作者个人的发明。根据上下文,我觉得前者是指功能测试,而后者就是非功能测试(例如性能测试,易用性测试,etc.)了。很奇怪这两个术语的命名。
    这里还提到Fault Insertion Tests,在组件和unit测试的层次,这个错误注入是很容易的,只要代码中有test seam就好了。但系统层面呢?之前曾经看到weibo中有人谈论Fault Injection test,当时也没有注意,有时间可以学习下。

3. 53/36页,Recorded Test是否可以用于非UI测试?

    作者提到记录回放式的自动测试一般都用于UI测试。因为UI经常改变,所以测试也不稳定。但如果用于非UI的接口测试呢?例如记录RPC请求和应答,然后验证呢?我没有做过,对后端系统的测试,都是写发送请求的代码,然后对应答做验证。后来发现作者也谈到了这一点,也大致是这个意思。但现实中很少这种工具,是不是因为API测试已经有足够“技术含量”了,能做的也有足够的编码能力,所以大家都不屑于不稳定的纪录回放式的测试了?当然,我觉得,如果有合适的库的支持,做这类测试所需的编码能力也就是大一水平。我倒更佩服从用户角度做探索性测试的。
    有人会拿实际的日志中的请求作为测试代码中的真实数据,然后在代码中作验证,或者把应答存放在一个文件中,然后跟之间真实数据录制的应答作diff。但我还是更喜欢根据需求来设计测试数据,这样子更有针对性。过程中可能会参考真实数据,但会过滤和整合。

4. 55/37页,图6.3中两类脚本的分类问题

     作者似乎认为xUnit只用于非UI(或者说API)的脚本测试。但事实上,现在很多UI的测试工具(WebDriver, Selenium, Puppet, Robotium, KIF(?), etc.),也都是基于xUnit框架的。对于UI测试,实在没有必要开发一套跟单元测试不同的测试框架。当然,作者后面谈到xUnit并不是那么合适做客户验收的测试,例如xUnit的测试失败之后就不执行下面的语句。但事实上,我认识的大多数人都在使用xUnit做各种级别的自动化测试。这只是个壳。看到后面,其实作者也认为xUnit可以用于客户测试,但最好是有一套领域专有的帮助库。在我理解,webdriver所推崇的page object就属于这类帮助库。
     另一个问题是,我觉得这里用”UI“也是不合适的,倒不如使用end-to-end测试,或者用户验收测试。UI也是可以独立的做单元测试,而不依赖于其后面的业务逻辑的。

5. 56/38页,到底什么是xUnit?

     xUnit是工具或库。它包括以下的特性:
     1) 能够通过test suite和test case来管理测试用例,可以方便的运行一个测试或者一组测试。
     2) 测试方法中可以通过统一的assert方法来验证测试是否通过。与之相对应,各种diff工具则需要通过人的参与才能判断测试是否通过。
     3) 一般而言,同一个测试方法中,一旦assert失败,就不继续运行后续的测试。但不同测试方法之间是应该相互独立的。事实上,有的xUnit框架,就可以在assert失败后继续运行测试。(例如gUnit的EXPECT和CHECK。)而不同测试方法之间的隔离,则完全靠编码人员,xUnit没有工具级别的支持,也没有办法提供统一的工具级别的支持,因为DOC是千差万别的。
     同时,作者也从另外一个角度阐述了它对xUnit的理解,那就是多数情况,基于xUnit的测试应该是从内部测试软件的,对象一般是单个的类或者对象。所以整本书中,作者集中于单元测试,而非客户测试。这是这些工具最初的用户,也是被称为Unit的原因吧。

6. 59/40页,什么是测试夹具(test fixture)?哪三种测试夹具?

    我的理解是,测试夹具是测试运行前的环境,具体包括SUT的初始状态和各种DOC的状态。我们会调用SUT和DOC的方法来初始化它们到测试所需的基础状态。
    之后的测试过程就是在这些状态基础上,通过执行测试操作,将SUT和DOC转换为新的状态。如果采用状态验证的方法,则是在验证阶段,验证新的状态是否等于预期。如果是行为验证,则是在测试过程中验证SUT和DOC之间的交互,这个交互也可以通过spy记录在最后的验证阶段来统一验证。
    作者认为有三种测试夹具,暂时新鲜(transient fresh),永久新鲜(pesistent fresh)和共享(shared)。
    暂时新鲜夹具在一个测试结束之后就被消失了,第二个测试根本没有办法访问之前测试的夹具,例如测试方法的局部变量或者测试类的非static成员变量,一个测试方法看不到另一个测试方法的这些变量本身,自然也无法共享。这里不能共享的只是变量本身,而不保证它们指向的可能是持久的内容。因为每次都是新鲜的,所以没有必要在tearDown中恢复默认状态(C++中该delete还是要在teardown中delete的,否则会有内存泄露,但这不会影响测试的正确性)
    永久新鲜则是可以在多个测试方法之间共享。例如一个类的static成员变量,是可以在同一个类的多个方法中共享的。全局变量也是可以共享的。即使使用局部变量或者普通成员变量,如果它们指向持久的数据,例如数据库表,那虽然这些变量的指针(或引用)的本身的值是不一样的,或者说是暂时新鲜的的,但它们指向的对象是被不同的方法共享的。这解释了“永久”的概念。我们称之位“新鲜”,是因为我们会在teardown中(并不限于teardown方法),将夹具恢复为初始状态。所以可以认为每次测试都是从一个干净的(新鲜的)环境(夹具)开始的。如果从生命期的角度来看,那暂时新鲜夹具的生命期跟测试方法是一样的,测试方法结束,暂时新鲜夹具也就结束了;永久新鲜夹具的生命期则长于测试方法,但因为每个测试结束我们都会手工将之恢复为新鲜状态,所以测试之间仍然是独立的。
    至于共享夹具,则是会在多个测试中共享,一个测试对该夹具的操作可能影响另外一个测试。我觉得就是个反模式。
    跟同事的讨论中,大家觉得作者对夹具的分类有点啰嗦。其实这些大家都在工作中遇到过,只是没有这些临时啊,永久啊的名词。好处是,有个统一的名字,以后讨论起来会方便很多。

7. 70/48页,如何测试UI,异步操作等难以测试的代码?

    UI和异步总是最难测试的部分。经常需要wait来等待操作完成,从而影响测试的速度和稳定性。作者提出的一种思路是使用humble object。也就是把复杂逻辑放在一个同步的不依赖于具体UI元素的类中,然后针对该类作单元测试。UI测试和异步测试只是一个壳。
    理论上这非常有用。前些日子自己使用MVP来重构一些Javascript的代码,在没有框架支持的情况下,把二者分离开,任何一个view现在都分为view和presenter。还要注意把UI和逻辑分别放在两个类中。如果UI逻辑复杂,那view和presenter中有很多交互,交互机制也要好好设计,代码上反而显得有些啰嗦。我觉得不分离view和presenter,直接发送Javascirpt的事件然后验证dom树,速度和效果也不错。可能自己的理解还是不够,或者用个MVP的框架会好很多。对MVC/P/VM自己一直没有真正的搞清楚,想得太多,做的太少,这个还要多实践。
    另外一个想到的问题时,面试时有时候会让别人针对一个依赖于数据库的复杂逻辑代码写单元测试。我期望的答案是使用test double。但有时候会有人回答先把复杂逻辑抽取出独立的函数,然后在外层的wrapper函数中调用复杂逻辑函数,并跟数据库系统交互。现在想想,很多情况下,这是个更好的方案。代码和测试代码都更容易理解。

8. 66/45页,观察点和控制点?

    控制点用来改变SUT和DOC的状态,观察点用来获取SUT或者DOC的状态,或者获取SUT和DOC之间的交互。
    从测试代码可读性的角度来看,最好都通过SUT的方法来观察和控制SUT。这样子可以通过测试代码很容易的理解SUT的使用方法和预期行为。
    如果SUT依赖于一个普通DOC,那我们必须通过一定参数构造DOC,也可能通过一些DOC的函数来操作和验证DOC的状态。我觉得DOC越少操作,可读性会越好。从设计的角度看,SUT的使用者也不该过度关注其依赖的组件。可能需要的只是在初始化时以特定参数构造,以及验证其操作后的状态。
    如果SUT依赖于比较heavy的DOC,必须要进行替换。那一个跟真实DOC接口相同的fake的对象会有跟真实系统类似的可读性。临时stub的效果会稍微差一些。依赖行为的mock则最差,虽然很多情况必须这么做。
    但也不能说使用test double就一定会导致可读性差。如果有两个包,包A调用包B。哪怕B都是轻量的操作,那在测试A时替换掉B,测试代码的可读性大概也会不错。而且使用这种方式,便于两个包的并行开发。
    一个扩展问题是,如果同一个包中,A依赖于B,B依赖于C。C比较heavy,现在测试对象是A。我是跟替换B呢还是C呢?<Javascript TDD>的作者曾经讨论过这个问题,他在不同的测试中使用不同的方法。而我现在倾向于替换C。一个系统中真正heavy的组件不会很多,如果我们对每个heavy的对象都有一个fake的实现,那单元测试中对mock框架的依赖会大大减少。但也没有很强烈的理由,现实中也两者都会用,还是要通过实践慢慢体会。同时我觉得如果自己实现了一个heavy的类,如果能提供一个light的供单元测试的同样接口的实现,那testability就非常之好。

9. 67/47页,层交叉测试的三种实现?

    这个在<.net单元测试艺术>中讲的更明确。第一种方法是依赖注入的方式,第二种是子类化,第三种是在工作代码中通过flag来开关测试或产品。当时自己还很方案子类化这种方式,但后来自己和别人都实践了下,觉得还真是方便。我觉得前两种的区别类似于strategy和template method二者的区别。前者使用delegation,后者使用inheritation。

你可能感兴趣的:(test,unit)