我想用TDD实现一个相当复杂的算法(用Java实现)。用于翻译自然语言的算法称为stack decoding。
当我尝试这样做时,我能够编写并修复一些简单的测试用例(空翻译,一个单词等等),但是我无法达到我想要的算法的方向。我的意思是我无法弄清楚如何在婴儿步骤中编写大量的算法。
这是算法的伪代码:
1: place empty hypothesis into stack 0
2: for all stacks 0...n − 1 do
3: for all hypotheses in stack do
4: for all translation options do
5: if applicable then
6: create new hypothesis
7: place in stack
8: recombine with existing hypothesis if possible
9: prune stack if too big
10: end if
11: end for
12: end for
13: end for
我是否错过任何可以完成婴儿步骤的方法,或者我是否应该获得一些保险并执行主要实施?
答案 0 :(得分:2)
通过关注实现(算法),你犯了一个错误。相反,首先想象你有一个神奇的类,它完成了算法执行的工作。它的API是什么?它的输入是什么,它的输出是什么?输入和输出之间所需的连接是什么?您希望将算法封装在此类中,并将您的问题重新生成为生成此类。
在这种情况下,它似乎输入是一个被标记化的句子(分成单词),输出是一个标记化的句子,它已被翻译成另一种语言。所以我想API就是这样的:
interface Translator {
/**
* Translate a tokenized sentence from one language to another.
*
* @param original
* The sentence to translate, split into words,
* in the language of the {@linkplain #getTranslatesFrom() locale this
* translates from}.
* @return The translated sentence, split into words,
* in the language of the {@linkplain #getTranslatesTo() locale this
* translates to}. Not null; containing no null or empty elements.
* An empty list indicates that the translator was unable to translate the
* given sentence.
*
* @throws NullPointerException
* If {@code original} is null, or contains a null element.
* @throws IllegalArgumentException
* If {@code original} is empty or has any empty elements.
*/
public List<String> translate(List<String> original);
public Locale getTranslatesFrom();
public Locale getTranslatesTo();
}
即战略设计模式的一个例子。所以你的任务变成了,而不是&#34;我如何使用TDD来实现这个算法&#34;而是&#34;我如何使用TDD来实现战略设计模式的特定情况&#34;。
接下来,您需要考虑使用此API的一系列测试用例,从最简单到最难。也就是说,要传递给translate
方法的一组原始句子值。对于每个输入,您必须在输出上给出一组约束。翻译必须满足这些限制。请注意,已经对输出有一些限制:
不为空;不是空的;不包含null或空元素。
您需要一些确定算法应该输出的例句。我怀疑你会发现很少有这样的句子。安排这些测试从最容易通过到最难通过。在您实施Translator
类时,这将成为您的TODO列表。
你会发现让你的代码通过了很多这些案例非常困难。那你怎么能彻底测试你的代码呢?
再看看算法。它很复杂,translate
方法不直接完成所有工作。它将委托其他类进行大部分工作
将空假设放入堆栈0
您需要Hypothesis
课吗?一个HypothesisStack
类?
所有翻译选项
您需要TranslationOption
课吗?
如果适用,那么
是否有方法TranslationOption.isApplicable(...)
?
如果可能,与现有假设重新组合
是否有Hypothesis.combine(Hypothesis)
方法?一个Hypothesis.canCombineWith(Hypothesis)
方法?
如果太大则修剪堆栈
是否有HypothesisStack.prune()
方法?
您的实施可能需要额外的课程。您可以使用TDD单独实现每个。您对Translator
类的一些测试最终将成为集成测试。其他类比Translator
更容易测试,因为它们将对它们应该做的事情进行精确定义的狭义定义。
因此,推迟实施Translator
,直到您实现了它委派给的那些类。也就是说,我建议您编写代码自下而上而不是自上而下。编写实现您给出的算法的代码成为最后一步。在那个阶段,您可以使用类来编写实现,使用的Java代码看起来非常类似于算法的伪代码。也就是说,translate
方法的主体只有大约13行。
您的翻译算法是通用的;它可以用于在任何一对语言之间进行翻译。我想那些适用于翻译特定语言对的内容是for all translation options
和if applicable then
部分。我猜后者可以通过TranslationOption.isApplicable(Hypothesis)
方法实现。那么使算法特定于特定语言的原因是生成翻译选项。摘要指向类所委托的工厂对象。像这样:
interface TranslationOptionGenerator {
Collection<TranslationOption> getOptionsFor(Hypothesis h, List<String> original);
}
现在到目前为止,您可能已经考虑过在真实语言之间进行翻译,以及所有令人讨厌的复杂性。但是,您不需要这种复杂性来测试您的算法。您可以使用一对假语言来测试它,这种语言比真实语言简单得多。或者(等效地)使用不像实际那样丰富的TranslationOptionGenerator
。使用依赖注入将Translator
与TranslationOptionGenerator
相关联。
现在考虑算法必须处理的TranslationOptionGenerator
的一些最极端的简单案例:
您可以使用它来生成不需要某些循环的测试用例,或者不需要进行isApplicable
测试。
将这些测试用例添加到您的TODO列表中。您将不得不编写假的简单TeranslatorOptionGenerator
对象供这些测试用例使用。
答案 1 :(得分:1)
TL; DR:从零开始,不写任何测试不需要的代码。然后你不得不TDD解决方案
当通过TDD构建一些东西时,你应该从零开始然后实现测试用例,直到它完成你想要的东西。这就是他们称之为红绿重构的原因。
您的第一个测试是查看内部对象是否有0个假设(空实现将为null。[红色])。然后初始化假设列表[绿色]。
接下来,您将编写一个检查假设的测试(它刚刚创建[红色])。实施“如果适用”逻辑并将其应用于一个假设[绿色]。
您编写的测试表明,当假设适用时,请创建一个新假设(检查是否存在适用于[红色]假设的1个假设)。实现创建假设逻辑并将其粘贴在if体中。 [绿色]
相反,如果假设不适用,则不做任何事情([绿色])
请跟随该逻辑,随着时间的推移单独测试算法。使用不完整的类比完整的类更容易。
答案 2 :(得分:1)
这里的关键是要了解&#34;婴儿步骤&#34;并不一定意味着一次只编写少量的生产代码。你也可以写很多,只要你通过一个相对较小的&amp;简单的测试。
有些人认为TDD只能通过一种方式应用,即通过编写单元测试。这不是真的。 TDD没有规定你应该写的那种测试。使用运行大量代码的集成测试来完成TDD是完全有效的。但是,每个测试都应该集中在一个明确定义的场景中。这种情况是&#34;婴儿步骤&#34;这真的很重要,而不是测试可以运用的课程数量。
就个人而言,我只使用集成级别测试开发了一个复杂的Java库,严格遵循TDD流程。很多时候,我创建了一个非常小而简单的集成测试,最终需要花费大量的编程工作来完成传递,需要更改几个现有的类和/或创建新的类。在过去5年多的时间里,这对我来说运作良好,到目前为止我已经进行了1300多次此类测试。