标签 | TDD Java
字数 | 3219字
阅读 | 9分钟
说明:本讲义是我在ThoughtWorks作为咨询师时,为客户开展TDD Code Kata而编写。案例为Guess Number,案例需求来自当时的同事王瑜珩。当时,我们共同在ThoughtWorks的Zynx交付团队,为培养团队TDD能力进行训练时,引入了本案例。讲义中给出的代码问题则来自客户方的受训学员,可谓“真实的代码坏味道”。个人认为TDD不只是开发方法,还应该是设计方法,因此讲义中包含了诸多设计原理、思想和原则。
在编写第二个测试时,由于测试样本与之前的测试完全不一样,之前的简单实现就不能满足新增的测试了。事实上,测试就是要去验证实现逻辑,这其中最重要的测试目标就是分支。不同的分支可能会返回不同的结果,如果我们根据分支来设计测试,就能有效保障实现的正确性。这称为“三角测试法”。
常见问题:
我们选择的第二个任务为“随机生成答案”,这是一个独立的职责。编写测试类时,很容易驱动出AnswerGenerator类。关键在于,我们该如何编写单元测试来验证生成的结果。我们对结果的要求是:
学员容易将此职责直接分配给Answer
。然而,随机生成答案与创建一个答案适用于不同的场景,这对于Answer
的调用者而言,并不友好。尤其对于只需要答案的场景,还需要无端地引入对随机数的依赖,显然是不合理的。
编写测试方法的过程与前相似,仍然按照Given-When-Then模式来编写(若测试方法比较简单,可以不遵循这一模式,但思考的过程却应该按照该模式)。
在编写then部分的测试时,可能出现疑问。
问题:如何验证生成的答案是否正确?
我们已经将答案建模为Answer,因此AnswerGenerator的generate()方法要返回的对象类型为Answer。那么,我们怎么知道返回的Answer对象是合法的呢?一种做法是获取Answer的属性,然后再进行验证。那么,为了测试的验证而暴露这些属性,是否适合?
要完成对答案正确性的验证,直接暴露答案的属性是不妥当的,至少目前没有获取答案属性的需求。我们的做法是定义一个验证方法。这是否仍然属于为测试而定义行为的做法呢?这个问题有点像鸡与鸡蛋的哲学问题。我们应该还原到设计,看看这种手法是否改善了设计,如此即可。毕竟,这种对答案正确性的校验,也可以说是业务逻辑的一种。
说明:在开始编写“检查输入是否合法”任务时,你会发现,这里所谓多余的验证,就会派上用场。
这个验证方法可以是单纯的返回true或者false,但从需求来看,这个返回结果并没有很好地展现验证要求:究竟是因为数字超出了范围,还是出现了相同的数字?我个人更倾向于用自定义异常来表示生成的答案违背了这两条规则。因此,我们可以为Answer
定义一个validate()
方法,以验证生成的Answer是否满足规则要求;如果不符合,就抛出对应的异常。
随着JUnit版本的演化,先后提供了三种验证异常的机制。
问题:如何确定测试通过就意味着实现正确?
第二个任务看似简单,实则不然。原因在于这里产生了一个随机数。随机数带来了不确定性,它可能偶然地让测试通过了。也许,运行测试100次,前面的99次都通过了,最后一次失败,仍然视为失败。
生成随机数自然是调用Java的JDK。在单元测试环节中,倘若我们要测试的单元需要调用别的API,则在这个测试中,我们可以假定这个API是正确的。我们对Java JDK的正确性自然信心十足。那么,为何我们还要考虑测试的随机失败?这是因为在这个任务的测试中,我们测试的并非随机数的生成逻辑,而在于随机数的种子是否恰当,实现逻辑中是否判断了可能出现的错误数字?
由于生成随机数的逻辑并非确定无疑的,测试时我们就不能依赖于它。这正是Mock可以派上用场的时候。为此,我们需要将生成随机数的功能提取为类RandomIntGenerator,再注入到AnswerGenerator中。
public class AnswerGenerator { private RandomIntGenerator randomIntGenerator; public AnswerGenerator(RandomIntGenerator randomIntGenerator) { this.randomIntGenerator = randomIntGenerator; }} |
---|
该类的实现调用了Java提供的Random类,但在测试时,我们却可以通过Mock它的行为,使得返回的结果变为确定的数字:
@Test(expected = OutOfRangeAnswerException.class) public void should_throw_OutOfRangeAnswerException_which_is_not_between_0_and_9() { RandomIntGenerator randomIntGenerator = mock(RandomIntGenerator.class); when(randomIntGenerator.nextInt()).thenReturn(1, 2, 3, 10); AnswerGenerator answerGenerator = new AnswerGenerator(randomIntGenerator); answerGenerator.generate(); } |
---|
在实现第一个任务时,我们定义的Game接受了Answer对象作为游戏的答案。现在,我们定义了AnswerGenerator用以生成符合条件的随机答案。我们当然可以在调用该对象的generate()方法生成答案后,再将该答案作为构造函数参数传递给Game对象。但更好的做法是直接将AnswerGenerator作为构造函数参数传递给Game,在其内部调用它的generate()方法。
阅读系列文章:
一个完整的TDD演练案例(一)
❈ 题图来自Mono《插画太空馆》,绘画者Charlotte Ager,伦敦插画师。她的作品乍看起来凌乱而不拘,但在线条与色块的交错中却有着独特的能量和动感。