如何用TDD实现复杂算法

时间:2014-09-16 12:01:52

标签: java algorithm tdd

我想用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

我是否错过任何可以完成婴儿步骤的方法,或者我是否应该获得一些保险并执行主要实施?

3 个答案:

答案 0 :(得分:2)

TL; DR

  1. 将您的任务重新设置为测试API的实现。
  2. 由于算法是以概念(假设翻译选项)表示的,这些概念本身非常复杂,因此首先开发(使用TDD)自下而上编程封装这些概念的类。
  3. 由于算法不是特定于语言,而是抽象语言特定的操作(所有翻译选项,如果适用),将语言特定部分封装在单独的对象中,使用该对象的依赖注入,并使用该对象的简单虚假实现​​进行测试。
  4. 使用您对算法的了解来建议良好的测试用例。

  5. 测试API

    通过关注实现(算法),你犯了一个错误。相反,首先想象你有一个神奇的类,它完成了算法执行的工作。它的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 optionsif applicable then部分。我猜后者可以通过TranslationOption.isApplicable(Hypothesis)方法实现。那么使算法特定于特定语言的原因是生成翻译选项。摘要指向类所委托的工厂对象。像这样:

     interface TranslationOptionGenerator {
    
        Collection<TranslationOption> getOptionsFor(Hypothesis h, List<String> original);
    
     }
    

    现在到目前为止,您可能已经考虑过在真实语言之间进行翻译,以及所有令人讨厌的复杂性。但是,您不需要这种复杂性来测试您的算法。您可以使用一对假语言来测试它,这种语言比真实语言简单得多。或者(等效地)使用不像实际那样丰富的TranslationOptionGenerator。使用依赖注入TranslatorTranslationOptionGenerator相关联。

    使用简单的假实现而不是现实的同事

    现在考虑算法必须处理的TranslationOptionGenerator的一些最极端的简单案例:

    • 例如,当没有空假设的选项时
    • 只有一个选项
    • 只有两种选择。
    • 英语到Harappan翻译。没有人知道如何翻译成Harappan,所以这位翻译人员表示每次都无法翻译。
    • 身份翻译:将英语翻译成英语的翻译。
    • 有限的英语到新生的翻译:它只能翻译句子&#34;我很饿&#34;,&#34;我需要改变&#34;和#34;我累了#34;它转化为&#34; Waaaa!&#34;。
    • 一个简单的英国到美国英语翻译:大多数单词是相同的,但有一些有不同的拼写。

    您可以使用它来生成不需要某些循环的测试用例,或者不需要进行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多次此类测试。