我也在Calc 中传递实例而不是使用一个局部变量,因此我知道我经常传递一个实例,而且这个实例是调用测试将其初始化的。当你想要改变对象状态时你可能想要做同样的事情,举个例子来说,当在测试下或者在将会传递给测试的对象下配置特殊对象时,可以使用特殊的Configure_XX方法。这些方法应该能够解释他们配置一个对象将会用来做什么用。Figure 3之中的代码就是以上方法的实例。
这个测试拥有很多设置代码可以用来处理向注册管理器对象中添加初始状态,它是这个测试类之中的成员。在此的确也有一些重复。Figure 4显示了在初始代码之外这些事例在因式分解之后将会如何变化。
修订测试具有非常高的可读性和稳定性。仅仅需要注意的是不要那么的refactor你的测试,他们可能会以一个单一的,不可读的代码行作为结束。应该注意的是我在这里可能依然使用一个Verify_XX 方法,但是这并不是我真正要在这里加以说明的。
消除测试之间的依赖关系
一个测试应该能够自我独立。它不应该与其他测试相关联,也不应该依赖任何具有特殊运行顺序的测试,它应该能够获得你所写的所有测试,可以随意运行所有测试或者只运行其中的一部分,并且是以任何顺序,而且要能够确保它们无论怎样都应该正确的运行。如果你不能够执行这个规则,你将会只在某种特殊的情况下按照预期的表现来运行的状况下结束你的测试。这样子的话,当你在最终期限下与此同时你还想确定你没有向系统之中引进新的问题的时候,当然就会出现问题。你可能很困惑而且考虑着是不是你的代码出现问题,这时,在事实上,问题其实仅仅是你的测试运行顺序所引起的。因此,你可能开始错过了一些在测试中失败的结果而且使它越写越少。这将会是个长期的过程。
如果你从一个测试调出至另一个测试之中,你应该在它们之间创建一个从属关系。你本质上说是在一个测试中测试两个事物(我将会在下一章中解释为什么这会成为一个问题)。就另一方面来说,如果你有测试B,它与测试A 所产生的状态是不相关的,那么你会陷入“顺序”陷阱之中。如果你或者其他人想要改变测试A,测试B将会暂停而且你不知它暂停的原因。对这些故障进行故障处理会浪费很多时间。
使用<TestInitialize()> 和<TestCleanup()>方法是本质上能够获得更好的测试隔离。确定你的测试数据时刻是最新的,而且测试下对象的也具有新的实例,而且所有的状态可以提前预知,而且无论你的测试在任何地方或者任何时间被运行,运行的情况都是相同的。
在一个单独单元测试中避免多重声明
我们将声明故障看作一个程序弊病的象征且声明被当作软件体的指示点或者“血液检查”。你可以找到越多的症状,程序弊病就越可以轻松的被诊断和排除掉。如果你在一个测试中定义了多重声明,只有第一个故障声明将会以抛出异常的方式显示出来。请参考下面插图之中的测试代码:
<TestMethod()> _
Public Sub Sum_AnyParamBiggerThan1000IsNotSummed()
Assert.AreEqual(3, Sum(1001, 1, 2)
Assert.AreEqual(3, Sum(1, 1001, 2) ' Assert fails
Assert.AreEqual(3, Sum(1, 2, 1001) ' This line never executes
End Sub
你可能没有发现以上代码之中其他可能的征兆。在一个故障之后,并发的声明不会被执行。这些不能生效的声明可能提供了有价值的数据(或者征兆)可能能够帮助你很快的集中的焦点而且发现潜在的问题。因此在一个独立的测试中运行多重声明增加了具有很少价值复杂性。另外,声明应该被独立的运行,我们应该设置自我独立的单元测试以使得你具有能够很好的发现错误的机会。
创建易读性测试
如果你以前写过单元测试,你是否在单元测试上写了一个好的声明行?可许不是这样的,大多数开发者并不厌烦去写一个好的声明因为他们更加关心去写测试。
假设你是团队中的一个新的开发者,你试图读一个单元测试。连接这个:
<TestMethod()> _
Public Sub TestCalcParseNegative()
Dim c As New Calc
?Assert.AreEqual(1000, c.Parse("-1, -1000")
End Sub
作为一个简单的练习,如果你理解了上例中Calc分列方法的用法,你很可能可以进行很好的推测,但是他可以简单的作为人员数量的用例使得输出结果为1000:
在组中返回最大的负数作为一个正数。
如果数字是负数且返回值为剩下几个数的总和作为一个正数,那么忽略第一个数字。
返回相互作乘积运算而得的数字。
现在请参考下面在单元测试之中的小改动:
<TestMethod()> _
Public Sub Parse_NegativeFirstNum_ReturnsSumOfTheRestAsPositive()
Dim c As New Calc
Dim parsedSumResult As Integer = c.Parse("-1", "-1000")
?Const SUM_WITH_IGNORED_FIRST_NUM As Integer = 1000
Assert.AreEqual(SUM_WITH_IGNORED_FIRST_NUM, parsedSumResult)
End Sub
这个是不是比较容易理解呢?当声明消息消失之后,表达意图最合适的地方就是测试的名字。 如果你广泛的使用了它,你将会发现你不再需要读测试代码就能明白代码测试的目的所在。事实上,你经常根本不需要写任何注释,因为代码,特别是那些带着实例的,他们自己是证明自己的。
名字包含了三部分内容: 测试下方法的名字(解析),测试下的状态或者规则(带着第一个负数传递一个字符串),以及预期的输出或者运行情况(剩余数字的总和以一个正数的形式返回)。需要注意的是我从名称中将Test以及Calc这两个词删除。我已经知道这是一个属性的测试因此在此没有重复此信息的必要。我也知道这是一个在Calc类中的测试因为测试类经常是写给一个特殊类的(这个类也许已经被命名为CalcTests)。
名字也许会很长,但是又有谁在乎呢?它读起来更像是一个标准英语的句子而且它使得一个新来的开发者更容易明白测试的内容。更是这样,当这个测试发生故障时,我们甚至不需要调试代码就可以知道问题究竟出在哪里。
需要注意的是,我已经在前面分别实际演示了通过在不同行中创建一个结果变量的方法从声明操作中进行分解操作。这样做至少有两个理由。第一个理由是,你可以为一个变量分配一个可读性强的名字,它可以包含结果,这样可以使你的声明行非常易于理解以及易于读。第二点是,测试下与对象相反的invocation 可能非常的长,它可能会使你的声明行延伸出屏幕的边缘之外,这样导致测试者向右滚屏。就我个人而言,我认为这个是非常恼人的。
我在我的测试中使用了很多常量以确保我的声明读起来像一本书。在先前的例子之中,你可以读到声明中说:“确保分解总数是与忽略第一个数后所得总和是相等的。” 为你的变量取一个很好的名字能够在某些程度上弥补对于测试的命名不足。
当然,有时一个声明 消息是在一个单元测试中传递intent的最好的方法。 一个好的声明消息始终能够解释什么因该会发生或者什么发生了而且为什么会出错。举个例子来说,“分列应该忽略掉第一个数字如果这个数字是个负数的话”,“分列不能够忽略掉第一个负数”,还有“X调用对象Y标记错误”这些都是有用的声明消息,它们很清晰的描述了结果的情况。
在你的设置方法中避免部分相关的代码
一个<TestInitialize()> 方法是样例成员变量在测试中使用的一个好地方。你所有的测试,只有在一部分的测试中避免变量。他们可以为测试设置本地变量。如果你创建了部分相关的实例作为类的成员,用来在测试中简单的避免创建的副本,你应该使用在文章前面解释的工厂方法,使用部分相关变量使得你的代码和设置方法缺少易读性。一旦变量在一个或者每个测试中使用,那么他应该是<TestInitialize()> 方法的一个成员和变量。
Figure 5 展现了一个拥有两个成员变量的类的测试。但是他们中的一个(cxNum)只被部分使用。Figure 6 展现了如何在测试中替换代码从而使它更加易读的方法。
总结
就像你所看到的,写单元测试并不是一个微不足道的任务,如果步骤正确,单元测试可以为开发者的生产力和代码的质量带来令人惊讶的提高,他可以帮助你去创建的应用程序含有更少的错误,同时也可以便于其他的开发者去洞察你的代码,但是他也需要在之前承担一个义务,确认遵循一些简单的规则。当方法并不是很好时,单元测试则可能达到一个相反的结果,从而浪费您的时间,并且使测试过程更加复杂。
文章来源于领测软件测试网 https://www.ltesting.net/