您应该如何TDD Yahtzee游戏?


36

假设您正在编写Yahtzee游戏TDD风格。您要测试代码部分,确定一组五个压模辊是否满座。据我所知,在进行TDD时,您遵循以下原则:

  • 首先编写测试
  • 写出最简单可行的方法
  • 优化和重构

因此,初始测试可能如下所示:

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 1, 1, 2, 2);

    Assert.IsTrue(actual);
}

当遵循“写出可能的最简单的IsFullHouse方法”时,您现在应该这样编写方法:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
    {
        return true;
    }

    return false;
}

这将导致绿色测试,但实现不完整。

您是否应该对整个房屋进行每个可能的有效组合(值和位置)的单元测试?看来,这是绝对确保您的IsFullHouse代码已经过完全测试和正确的唯一方法,但这听起来也很疯狂。

您将如何对此类内容进行单元测试?

更新资料

Erik和Kilian指出,在初始实现中使用文字以获得绿色测试可能不是最好的主意。我想解释为什么这样做,而该解释不适合评论。

我在单元测试(尤其是使用TDD方法)方面的实践经验非常有限。我记得在Tekpub上观看过Roy Osherove的TDD Masterclass录音。在其中一集中,他构建了String Calculator TDD风格。字符串计算器的完整规范可以在这里找到:http : //osherove.com/tdd-kata-1/

他从这样的测试开始:

public void Add_with_empty_string_should_return_zero()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("");

    Assert.AreEqual(0, result);
}

这导致该Add方法的第一个实现:

public int Add(string input)
{
    return 0;
}

然后添加此测试:

public void Add_with_one_number_string_should_return_number()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("1");

    Assert.AreEqual(1, result);
}

Add重构了该方法:

public int Add(string input)
{
    if (input.Length == 0)
    {
        return 0;
    }

    return 1;
}

每一步之后,罗伊都会说“写出最简单的方法”。

因此,我想尝试制作TDD风格的Yahtzee游戏时会尝试这种方法。


8
“写出最简单可行的方法”实际上是缩写。正确的建议是“写出最简单的东西,它不会完全死于脑子,而且显然是不正确的 ”。所以,不,您不应该写if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Carson63000

3
谢谢您总结埃里克(Erik)的答案,无论是以较少争议的方式还是以文明的方式。
克里斯托弗·克莱斯

1
像@ Carson63000一样,“写出最简单的方法”实际上是一种简化。那样思考实际上很危险;它导致臭名昭著的Sudoku TDD崩溃(google it)。当盲目地遵循时,TDD确实是脑筋急转弯:您不能盲目地做“最简单的事情”来概括一个非平凡的算法……您必须真正思考!不幸的是,即使是所谓的XP和TDD大师有时也会盲目跟进...
Andres F.

1
@AndresF。请注意,在不到三天的时间里,您的评论在Google搜索中的排名要高于有关“ Soduko TDD崩溃”的大部分评论。尽管如此,如何不解决数独总结了一下:TDD是为了质量,而不是正确性。您需要在开始编码之前解决算法,尤其是使用TDD时。(并不是说我也不是代码优先程序员。)
Mark Hurd

Answers:


40

这个问题已经有很多不错的答案,我已经评论并推荐了其中几个。不过,我想补充一些想法。

灵活性不适合新手

OP明确指出他没有 TDD经验,我认为必须考虑到一个很好的答案。在Dreyfus技能获取模型的术语中,他可能是新手。成为新手并没有错-当我们开始学习新东西时,我们都是新手。但是,Dreyfus模型所解释的是,新手的特征在于

  • 严格遵守教导的规则或计划
  • 不行使酌情决定权

那不是对人格缺陷的描述,因此没有理由为此感到ham愧-这是我们所有人都需要经历的一个阶段,以学习新知识。

对于TDD也是如此。

我在这里同意许多其他答案,即TDD不必过于教条,有时以其他方式工作可能会更有益,但这对刚起步的人没有帮助。没有经验的人如何进行自由裁量的判断?

如果新手接受了有时不执行TDD的建议,那么他或她如何确定什么时候可以跳过TDD?

没有经验或指导,新手唯一能做的就是每次变得太困难时就退出TDD。那是人的本性,但不是学习的好方法。

听测试

每当变得困难时就跳出TDD就是错过TDD最重要的好处之一。测试可提供有关SUT API的早期反馈。如果测试难以编写,则表明SUT难以使用是一个重要标志。

这就是为什么GOOS最重要的消息之一就是:听您的测试!

对于这个问题,在看到Yahtzee游戏的拟议API以及在此页面上可以找到有关组合技术的讨论时,我的第一反应是,这是有关API的重要反馈。

API是否必须将骰子掷骰表示为整数的有序序列?对我来说,有原始迷恋的味道。这就是为什么我很高兴看到塔尔斯泰思的答案暗示了开设一门Roll课程的原因。我认为这是一个很好的建议。

但是,我认为对该答案的某些评论是错误的。TDD所建议的是,一旦您意识到Roll类是一个好主意,就可以暂停原始SUT上的工作,然后开始进行TDD Roll类学习。

尽管我同意TDD的目标不是全面测试,而是更关注“幸福的道路”,但它仍有助于将系统分解为可管理的单元。上一Roll堂课听起来就像您可以轻松完成TDD。

然后,一旦Roll类别充分发展,您将回到原始SUT并根据Roll输入充实它。

测试助手的建议不一定暗示随机性,它只是使测试更具可读性的一种方法。

根据Roll实例对输入进行建模的另一种方法是引入Test Data Builder

红色/绿色/重构是一个三阶段过程

我同意大家的普遍看法,即(如果您在TDD方面有足够的经验),您不必严格遵守TDD,但对于Yahtzee演习,我认为这是一个非常糟糕的建议。尽管我不了解Yahtzee规则的详细信息,但在这里我没有令人信服的论点,即您不能严格遵循Red / Green / Refactor流程,而仍能获得适当的结果。

这里大多数人似乎忘记的是红色/绿色/重构过程的第三阶段。首先,您编写测试。然后,编写通过所有测试的最简单的实现。然后您重构。

在第三种状态下,您可以在这里发挥所有专业技能。这是您可以反思代码的地方。

但是,我认为应该只声明“写出可能的最简单的事情,而这并非完全是脑残,显然是不正确的 ”。如果您(认为您)事先对实现有足够的了解,那么除了完整解决方案之外的所有事情显然都是不正确的。就建议而言,这对新手来说毫无用处。

真正应该发生的是,如果您可以通过所有明显不正确的实现使所有测试通过,那就是您应该编写另一个测试反馈

令人惊讶的是,这样做经常使您实现与您最初想到的实现完全不同的实现。有时,这样增长的替代方案可能会比您的原始计划更好。

严谨是一种学习工具

只要学习,坚持严格的过程(如红色/绿色/重构)就很有意义。它迫使学习者不仅在简单的时候,而且在困难的时候都获得了TDD的经验。

只有掌握了所有困难的部分后,您才可以就何时偏离“真实”道路做出明智的决定。那就是您开始形成自己的道路的时候。


TDD的另一个新手,对尝试它的所有常见疑虑。有趣的是,如果您可以通过所有明显不正确的实现使所有测试通过,那就是您应该编写另一个测试反馈。似乎是一种解决这种想法的好方法,即测试“ braindead”实现是不必要的工作。
shambulator 2014年

1
哇谢谢你。人们真的倾向于告诉TDD(或任何学科)的初学者“不要担心规则,只要去做最好的事情”,我就感到非常害怕。如果您没有知识或经验,怎么知道什么才是最好的?我还要提及转换优先级原则,或者随着测试变得更加具体,该代码应该变得更加通用。最顽固的TDD支持者(例如bob叔叔)不会支持“只为每个测试添加新的if语句”的想法。
萨拉

41

作为免责声明,这是我在实践中使用的TDD,并且正如Kilian恰当地指出的那样,对于任何建议有一种正确的实践方法的人,我都会保持警惕。但这也许会帮助您...

首先,您可以通过的最简单的操作是:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    return true;
}

这很重要,因为不是因为某些TDD做法,而是因为硬编码所有这些文字并不是一个好主意。使用TDD时,最困难的事情之一就是它不是一种全面的测试策略,它是一种防止回归并在保持代码简单的同时标记进度的方法。这是一种开发策略,而不是测试策略。

我之所以提到这种区别,是因为它有助于指导您应该编写哪些测试。回答“我应该写什么测试?” 是“需要以所需方式获取代码的任何测试。” 将TDD视为帮助您弄清楚算法和代码推理的一种方式。因此,考虑到您的测试和我的“简单绿色”实现,接下来要进行什么测试?好吧,您已经建立了一个满屋的东西,那么什么时候不满屋呢?

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 2, 3, 4, 5);

    Assert.IsFalse(actual);
}

现在,您必须找出某种方法来区分两个有意义的测试用例。我本人将对“做最简单的事情来使测试通过”做些澄清的信息,然后说“做最简单的事来使测试通过,从而进一步实现您的实现”。编写失败的测试是更改代码的借口,因此,当您去编写每个测试时,您应该问自己:“我的代码不执行我想要做的事情,以及如何暴露这种缺陷?” 它还可以帮助您使代码健壮并处理边缘情况。如果呼叫者输入废话怎么办?

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(-1, -2, -3, -4, -5);

    //I dunno - throw exception, return false, etc, whatever you think it should do....
}

综上所述,如果您正在测试值的每种组合,则几乎可以肯定是做错了(并且可能会随着条件条件的组合爆炸而结束)。当涉及到TDD时,您应该编写最少数量的测试用例,以获得所需的算法。您编写的所有其他测试将从绿色开始,因此从本质上成为文档,而不严格地属于TDD流程。如果需求发生变化或暴露了错误,您将只编写进一步的TDD测试用例,在这种情况下,您将通过测试记录缺陷,然后使其通过。

更新:

为了回应您的更新,我将其作为评论开始,但是它开始变得相当长...

我会说问题不在于字面量和句点的存在,而在于“最简单的”东西是由五部分组成的条件。考虑一下,五部分条件实际上是相当复杂的。通常在红色到绿色的步骤中使用文字,然后在重构步骤中将它们抽象为常量,或者在以后的测试中将其归纳。

在我使用TDD的旅程中,我意识到要区分一个重要的区别-混淆“简单”和“钝”是不好的。就是说,当我刚开始的时候,我看着人们在做TDD,我以为“他们只是在做最愚蠢的事情来使测试通过”,我模仿了一段时间,直到我意识到“简单”是微妙的不同。而不是“钝角”。有时它们重叠,但通常不重叠。

因此,如果我给我印象以为文字的存在是问题,那不是很抱歉。我要说的是5个条款的条件复杂性。您的第一个红色到绿色只是“恢复原状”,因为那确实很简单(巧合的是,很钝)。下一个具有(1、2、3、4、5)的测试用例将必须返回false,这是您开始在后面留下“钝角”的地方。您必须问自己“为什么(1、1、1、2、2)满屋而(1、2、3、4、5)不是?” 您能想到的最简单的事情可能是,一个具有最后一个序列元素5或第二个序列元素2,而另一个没有。这些很简单,但是(不必要地)它们也很钝。您真正想开车去的是“他们有多少个相同的数字?” 因此,您可以通过检查是否重复来获得第二项测试通过。在一个重复中,您有一个完整的房子,而在另一个中则没有。现在测试通过了,您编写了另一个具有重复但还不足以进一步完善算法的测试用例。

您可以随便使用字面量,也可以不这样做,也可以。但是总的想法是,随着您添加更多案例,算法会“有机地”增长。


我已经更新了我的问题,以添加更多有关为什么我从字面方法开始的信息。
克里斯托弗·克莱斯

9
这是一个很好的答案。
tallseth

1
非常感谢您的周到和解释清楚的答案。现在我想起来,这实际上很有意义。
克里斯托弗·克莱斯

1
彻底的测试并不意味着要测试所有组合……这很愚蠢。对于这种特殊情况,请使用一或两个特定的满屋和几个非满屋。还有可能引起麻烦的任何特殊组合(即5种)。
Schleis

3
+1罗伯特·C·马丁(Robert C. Martin)的“转换优先权前提” cleancoder.posterous.com/the-transformation-priority-premise
Mark Seemann,

5

对于发烧的大脑来说,测试特定组合中的五个特定文字值并不是“最简单的”。如果解决一个问题确实是明显的(你计数是否有整整三又恰好两个的任何值),然后通过各种手段继续前进,该解决方案的代码,编写一些测试,这将是非常,非常不可能与意外满足您编写的代码量(即,不同的字面量和三元组和二元组的不同顺序)。

TDD格言确实是工具,而不是宗教信仰。他们的目的是让您快速编写正确的,结构合理的代码。如果有明显的格言阻碍了这一步,请直接跳到下一步。您的项目中将有很多不明显的地方可以应用。


5

Erik的回答很好,但我认为我可能会在测试写作中分享一些技巧。

从此测试开始:

[Test]
public void FullHouseReturnsTrue()
{
    var pairNum = AnyDiceValue();
    var trioNum = AnyDiceValue();

    Assert.That(sut.IsFullHouse(trioNum, pairNum, trioNum, pairNum, trioNum));
}

如果您创建一个Roll类而不是传递5个参数,则此测试会变得更好:

[Test]
public void FullHouseReturnsTrue()
{
    var roll = AnyFullHouse();

    Assert.That(sut.IsFullHouse(roll));
}

这样实现:

public bool IsFullHouse(Roll toCheck)
{
    return true;
}

然后编写此测试:

[Test]
public void StraightReturnsFalse()
{
    var roll = AnyStraight();

    Assert.That(sut.IsFullHouse(roll), Is.False);
}

一旦通过,编写以下代码:

[Test]
public void ThreeOfAKindReturnsFalse()
{
    var roll = AnyStraight();

    Assert.That(sut.IsFullHouse(roll), Is.False);
}

在那之后,我敢打赌,您不需要再写了(如果您认为这不是满屋子,可能是两双,也可能是yahtzee)。

显然,实现您的Any方法以返回符合条件的随机劳斯莱斯。

这种方法有一些好处:

  • 您无需编写测试的唯一目的就是阻止您陷入特定的价值
  • 测试真的很好地传达了您的意图(第一个测试的代码尖叫“任何满屋都返回true”)
  • 它可以使您迅速解决问题的根源
  • 有时它会注意到您没有想到的情况

如果采用这种方法,则需要改进Assert.That语句中的日志消息。开发人员需要查看导致失败的输入。
Bringer128

这不会造成鸡或蛋的困境吗?当您实现AnyFullHouse(也使用TDD)时,是否不需要IsFullHouse来验证其正确性?具体来说,如果AnyFullHouse有错误,则可以在IsFullHouse中复制该错误。
蜡翼

AnyFullHouse()是测试案例中的一种方法。您通常会TDD测试用例吗?不。而且,创建一个满屋子(或任何其他卷)的随机样本比测试其存在要容易得多。当然,如果您的测试存在错误,则可以将其复制到生产代码中。不过,所有测试都是如此。
tallseth

AnyFullHouse是测试用例中的“帮助器”方法。如果它们足够通用,辅助方法也将经过测试!
Mark Hurd

应该IsFullHouse真正回归true,如果pairNum == trioNum
recursion.ninja

2

我可以想到两种主要的测试方法:

  1. 再添加“一些”有效用例集的测试用例(〜5),并且相同数量的预期错误({1、1、2、3、3}是一个很好的用例。请记住,例如5个是被错误的实现识别为“相同的3加一对”)。此方法假定开发人员不仅尝试通过测试,而且实际上正确地实施了测试。

  2. 测试所有可能的骰子组(只有252个不同的骰子)。当然,这是假定您有某种方法知道预期的答案是什么(在测试中称为oracle。)这可能是相同功能或人类的参考实现。如果您真的要严格,手动编码每个预期结果可能是值得的。

碰巧的是,我曾经写过Yahtzee AI,当然必须知道规则。您可以在此处找到分数评估部分的代码,请注意,该实现是针对斯堪的纳维亚版本(Yatzy)的,并且我们的实现假定骰子是按排序顺序给出的。


一百万美元的问题是,您是否使用纯TDD派生Yahtzee AI?我敢打赌,你做不到;您必须使用领域知识,按照定义,它不是盲目的:)
Andres F.

是的,我想你是对的。这是TDD的一个普遍问题,除非您只想测试意外的崩溃和未处理的异常,否则测试用例需要预期的输出。
ansjob

0

这个例子确实错了。我们在这里谈论的是一个简单的功能,而不是软件设计。有点复杂吗?是的,所以您将其分解。而且,您绝对不会测试从1,1,1,1,1到6,6,6,6,6,6的所有可能的输入。有问题的功能不需要命令,只是一个组合,即AAABB。

您不需要200个单独的逻辑测试。您可以使用例如一个集合。几乎任何编程语言都内置了一种:

Set set;
set.add(a);
set.add(b);
set.add(c);
set.add(d);
set.add(e);

if(set.size() == 2) { // means we *must* be of the form AAAAB or AAABB.
    if(a==b==c==d) // eliminate AAAAB
        return false;
    else
        return true;
}
return false;

而且,如果输入的内容不是有效的Yahtzee掷骰,则应该投掷,就像没有明天一样。

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.