TDD测试驱动开发︱一个完整的TDD测试驱动开发演练案例
2022-11-06
来源:逸言 我是张逸
h结合fail()方法进行验证。这种方法带来的问题是验证逻辑太繁琐。
第二种方法是利用@Test的expected方法,通过指定异常类型值来验证。它的好处是简单直接,缺点是只能验证抛出异常的类型。
第三种方法是利用ExpectedException Rule。Rule可以更灵活地验证异常,包括异常类型和异常消息。我们也可以通过定义派生自TypeSafeMatcher的Matcher类,来验证异常的更多信息。
问题:如何确定测试通过就意味着实现正确?
第二个任务看似简单,实则不然。原因在于这里产生了一个随机数。随机数带来了不确定性,它可能偶然地让测试通过了。也许,运行测试100次,前面的99次都通过了,最后一次失败,仍然视为失败。
生成随机数自然是调用Java的JDK。在单元测试环节中,倘若我们要测试的单元需要调用别的API,则在这个测试中,我们可以假定这个API是正确的。我们对Java JDK的正确性自然信心十足。那么,为何我们还要考虑测试的随机失败?这是因为在这个任务的测试中,我们测试的并非随机数的生成逻辑,而在于随机数的种子是否恰当,实现逻辑中是否判断了可能出现的错误数字?
由于生成随机数的逻辑并非确定无疑的,测试时我们就不能依赖于它。这正是Mock可以派上用场的时候。为此,我们需要将生成随机数的功能提取为类RandomIntGenerator,再注入到AnswerGenerator中。
该类的实现调用了Java提供的Random类,但在测试时,我们却可以通过Mock它的行为,使得返回的结果变为确定的数字:
重构:组合Game与AnswerGenerator
在实现第一个任务时,我们定义的Game接受了Answer对象作为游戏的答案。现在,我们定义了AnswerGenerator用以生成符合条件的随机答案。我们当然可以在调用该对象的generate()方法生成答案后,再将该答案作为构造函数参数传递给Game对象。但更好的做法是直接将AnswerGenerator作为构造函数参数传递给Game,在其内部调用它的generate()方法。
开始第三个任务
之所以将“验证输入是否合法”放在第三个任务,是因为它不属于happy path的范畴。它属于辅助业务,重要性相对次之。
提示:对于第三个任务,可以采用Specification By Example的方式来考虑测试用例。
问题:参数 vs. 字段
学员在定义执行该任务的类时,一种可能性是将输入的答案作为类的构造函数参数。例如:
存在两个错误:
错误地判断了输入值的生命周期。什么内容应该放在构造函数中作为参数?换言之,构造函数参数与对象之间的关系是什么?之所以要作为构造函数参数,就是意味着在某种场景下这些参数值应该在创建该对象时就存在。这些参数值与对象“生死与共”,它们的生命周期是保持一致的。如果不是,就不应该作为构造函数的参数。你觉得输入应该作为构造函数吗?如果我要验证另一条输入应该怎么办?再创建一个InputValidator对象吗?
违反了阅读直觉。validate()方法验证谁?验证空吗?显然这样的接口违反了主-谓-宾的语法。
问题:封装的Answer与输入
既然已经封装了Answer对象,为何validate()方法还是要接收字符串类型的输入?阅读需求,已可寻求到答案。
问题:引入InputValidator类型是否有必要?
多数人会认为这里的验证逻辑与Answer相关,根据前面提到的“信息专家模式”,似乎应该将验证逻辑放到Answer中。然而,这里的需求明确地表示了,如果输入不符
免责声明:
1、IT项目管理界发布的所有资讯与文章是出于为业界传递更多信息之目的,并不意味着赞同其观点或证实其描述。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请浏览者仅作参考,并请自行核实相关内容。
2、本站部分内容转载于其他网站和媒体,版权归原作者或原发布媒体所有。如文章涉及版权等问题,请联系本站,我们将在两个工作日内进行删除或修改处理。敬请谅解!