当创建一个像AI这样的系统时,它可以非常迅速地采用许多不同的路径,或者实际上是具有多个不同输入的任何算法,则可能的结果集可能包含大量的排列。
创建一个输出许多很多结果排列的系统时,应该采用哪种方法来使用TDD?
当创建一个像AI这样的系统时,它可以非常迅速地采用许多不同的路径,或者实际上是具有多个不同输入的任何算法,则可能的结果集可能包含大量的排列。
创建一个输出许多很多结果排列的系统时,应该采用哪种方法来使用TDD?
Answers:
对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));
}
希望这可以为您提供有关如何设计应用程序的想法,以便可以强制进行排列,以便您可以测试所有边缘情况以及其他情况。
在这些情况下,它远不至于因复杂性而崩溃。它会促使您以较小的零件来考虑较大的问题,这将导致更好的设计。
不要着手尝试测试算法的每个排列。只需在测试后构建测试,然后编写最简单的代码即可使测试生效,直到您覆盖了基础为止。您应该明白我对解决问题的意思,因为我们鼓励您在测试其他部分时假冒问题的一部分,以免自己不得不为100亿个排列编写100亿个测试。
编辑:我想添加一个示例,但没有时间早。
让我们考虑一个就地排序算法。我们可以继续编写测试,这些测试涵盖数组的顶端,数组的底端以及中间的各种奇怪组合。对于每个对象,我们都必须构建某种对象的完整数组。这需要时间。
或者我们可以分四个部分解决问题:
第一个是问题的唯一复杂部分,但是通过从其余部分中抽象出来,您使问题变得简单得多。
几乎可以肯定,第二个对象是由对象本身处理的,至少在很多静态类型的框架中,至少可以有选择地使用一个接口来显示该功能是否已实现。因此,您无需测试。
第三个是非常容易测试的。
第四只处理两个指针,要求遍历类将指针移动,调用比较,并根据比较结果调用要交换的项。如果您伪造了前三个问题,则可以很轻松地进行测试。
我们如何在这里导致更好的设计?假设您将其简化并实现了冒泡排序。它可以工作,但是当您投入生产并且必须处理一百万个对象时,它太慢了。您要做的就是编写新的遍历功能并将其交换。您不必处理其他三个问题的复杂性。
您会发现,这就是单元测试和TDD之间的区别。单元测试人员会说这使您的测试变得脆弱,如果您已经测试了简单的输入和输出,那么您现在就不必为新功能编写更多的测试了。TDDer会说我已经适当地分离了关注点,以便我所拥有的每个班级都能做好一件事。
无法使用多个变量测试计算的每个排列。但这并不是什么新鲜事物,对于任何超出玩具复杂性的程序,它都是正确的。测试的重点是验证计算的属性。例如,对具有1000个数字的列表进行排序需要花费一些精力,但是可以轻松验证任何单个解决方案。现在,虽然有1000个!该程序可能的输入(类别),而您不能全部测试,仅随机生成1000个输入并验证输出是否确实排序就足够了。为什么?因为几乎不可能编写一个程序来可靠地对1000个随机生成的向量进行排序而又通常不正确(除非您故意操纵它以操纵某些魔术输入...)
现在,总的来说事情要复杂一些。确实存在一些错误,如果用户的用户名中有“ f”,并且星期几是星期五,则邮寄者不会向用户发送电子邮件。但是我认为试图预料这种怪异是浪费了精力。您的测试套件应使您对系统在输入期望值上所能达到的预期值充满信心。如果它在某些时髦的情况下确实很时髦,那么在您尝试第一个时髦的情况后,您会很快注意到,然后您可以针对该情况编写专门的测试(通常也将涵盖整个类似情况的类)。