对于具有许多排列的事物,如何进行TDD?


15

当创建一个像AI这样的系统时,它可以非常迅速地采用许多不同的路径,或者实际上是具有多个不同输入的任何算法,则可能的结果集可能包含大量的排列。

创建一个输出许多很多结果排列的系统时,应该采用哪种方法来使用TDD?


1
AI系统的整体优势通常是通过带有基准输入集的Precision-Recall测试来衡量的。该测试与“集成测试”大致相当。正如其他人提到的那样,它更像是“测试驱动算法研究”,而不是“测试驱动设计 ”。
rwong 2011年

请定义“ AI”的含义。这是一个比任何特定类型的程序都要研究的领域。对于某些AI实施,通常无法通过TDD测试某些类型的事物(即:紧急行为)。
史蒂文·埃弗斯,

@SnOrfus我的意思是最一般的,基本的决策机器。
妮可(Nicole)

Answers:


7

pdr的回答采取更实际的方法。TDD完全是关于软件设计而不是测试。您可以使用单元测试来验证工作。

因此,在单元测试级别,您需要设计单元,以便可以完全确定性的方式对它们进行测试。您可以采取使单位不确定的任何措施(例如随机数生成器)并将其抽象化来实现。假设我们有一个简单的示例,该示例确定移动是否正确:

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

这种方法很难测试,并且您真正可以在单元测试中验证的唯一一件事就是它的界限……但这需要大量尝试才能达到界限。因此,让我们通过创建一个接口和包装该功能的具体类来抽象掉随机化部分:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

Decider现在,该类需要通过其抽象即接口使用具体的类。这种处理方式称为依赖项注入(以下示例是构造函数注入的示例,但是您也可以使用setter进行此操作):

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

您可能会问自己,为什么这种“代码膨胀”是必要的。好吧,对于初学者来说,您现在可以模拟算法随机部分的行为,因为它Decider现在具有遵循IRandom“合同” 的依赖性。您可以为此使用模拟框架,但是此示例非常简单,可以自己编写代码:

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

最好的部分是,它可以完全替代“实际”的具体实现。代码变得易于测试,如下所示:

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

希望这可以为您提供有关如何设计应用程序的想法,以便可以强制进行排列,以便您可以测试所有边缘情况以及其他情况。


3

对于更复杂的系统,严格的TDD确实会导致一些故障,但是从实际意义上讲并没有太大关系-一旦您能够隔离单个输入,只需选择一些提供合理覆盖率的测试用例并使用它们即可。

这确实需要一些有关实现效果的知识,但这更多是理论上的问题-您极不可能构建由非技术用户详细指定的AI。它与通过对测试用例进行硬编码来通过测试属于同一类别-正式而言,测试是规范,并且实现既正确又是最快的解决方案,但这实际上从未发生。


2

TDD与测试无关,而与设计有关。

在这些情况下,它远不至于因复杂性而崩溃。它会促使您以较小的零件来考虑较大的问题,这将导致更好的设计。

不要着手尝试测试算法的每个排列。只需在测试后构建测试,然后编写最简单的代码即可使测试生效,直到您覆盖了基础为止。您应该明白我对解决问题的意思,因为我们鼓励您在测试其他部分时假冒问题的一部分,以免自己不得不为100亿个排列编写100亿个测试。

编辑:我想添加一个示例,但没有时间早。

让我们考虑一个就地排序算法。我们可以继续编写测试,这些测试涵盖数组的顶端,数组的底端以及中间的各种奇怪组合。对于每个对象,我们都必须构建某种对象的完整数组。这需要时间。

或者我们可以分四个部分解决问题:

  1. 遍历数组。
  2. 比较所选项目。
  3. 切换项目。
  4. 协调以上三个。

第一个是问题的唯一复杂部分,但是通过从其余部分中抽象出来,您使问题变得简单得多。

几乎可以肯定,第二个对象是由对象本身处理的,至少在很多静态类型的框架中,至少可以有选择地使用一个接口来显示该功能是否已实现。因此,您无需测试。

第三个是非常容易测试的。

第四只处理两个指针,要求遍历类将指针移动,调用比较,并根据比较结果调用要交换的项。如果您伪造了前三个问题,则可以很轻松地进行测试。

我们如何在这里导致更好的设计?假设您将其简化并实现了冒泡排序。它可以工作,但是当您投入生产并且必须处理一百万个对象时,它太慢了。您要做的就是编写新的遍历功能并将其交换。您不必处理其他三个问题的复杂性。

您会发现,这就是单元测试和TDD之间的区别。单元测试人员会说这使您的测试变得脆弱,如果您已经测试了简单的输入和输出,那么您现在就不必为新功能编写更多的测试了。TDDer会说我已经适当地分离了关注点,以便我所拥有的每个班级都能做好一件事。


1

无法使用多个变量测试计算的每个排列。但这并不是什么新鲜事物,对于任何超出玩具复杂性的程序,它都是正确的。测试的重点是验证计算的属性。例如,对具有1000个数字的列表进行排序需要花费一些精力,但是可以轻松验证任何单个解决方案。现在,虽然有1000个!该程序可能的输入(类别),而您不能全部测试,仅随机生成1000个输入并验证输出是否确实排序就足够了。为什么?因为几乎不可能编写一个程序来可靠地对1000个随机生成的向量进行排序而又通常正确(除非您故意操纵它以操纵某些魔术输入...)

现在,总的来说事情要复杂一些。确实存在一些错误,如果用户的用户名中有“ f”,并且星期几是星期五,则邮寄者不会向用户发送电子邮件。但是我认为试图预料这种怪异是浪费了精力。您的测试套件应使您对系统在输入期望值上所能达到的预期值充满信心。如果它在某些时髦的情况下确实很时髦,那么在您尝试第一个时髦的情况后,您会很快注意到,然后您可以针对该情况编写专门的测试(通常也将涵盖整个类似情况的类)。


假设您随机生成1000个输入,那么如何测试输出?当然,这种测试将涉及一些逻辑,而这些逻辑本身并未经过测试。那么您测试测试了吗?怎么样?重点是,您应该使用状态转换来测试逻辑-给定输入X,输出应该为Y。涉及逻辑的测试与测试的逻辑一样容易出错。从逻辑上讲,用另一个参数来证明一个参数的合理性会使您走上怀疑的回归之路-您必须提出一些主张。这些断言是您的测试。
伊扎基2015年

0

以边缘情况加上一些随机输入。

以排序为例:

  • 对一些随机列表进行排序
  • 取得已排序的清单
  • 取相反顺序的清单
  • 拿一个几乎排序的清单

如果它对这些输入快速起作用,那么您可以确定它对所有输入都适用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.