どのようにヤッツィーのゲームをTDDすべきですか?


36

YahtzeeゲームのTDDスタイルを書いているとしましょう。5つのサイコロのセットがフルハウスかどうかを判断するコードの一部をテストします。私の知る限り、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の録音を見たことを覚えています。エピソードの1つで、彼は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スタイルのヤッツィーゲームをしようとするときに、このアプローチを試してみると思いました。


8
「機能する最も簡単なものを書く」は実際には略語です。正しいアドバイスは、「完全に頭がおかしくなく、明らかに正しくない、可能な限り単純なことを書く」ことです。だから、いいえ、あなたは書くべきではありませんif (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
-Carson63000

3
エリックの答えを要約していただきありがとうございます。
クリストフクレス

1
@ Carson63000のように、「動作する最も単純なものを書く」ことは、実際には単純化です。そのように考えることは実際には危険です。悪名高い数独TDDの大失敗(グーグルit)につながります。盲目的にたどると、TDDは本当に頭が切れます。盲目的に「動作する最も単純なこと」を行うことによって、自明でないアルゴリズムを一般化することはできません...実際に考えなければなりません!残念ながら、XPおよびTDDのも、疑惑のマスターは、時には盲目的にそれに従ってください...
アンドレスF.

1
@AndresF。あなたのコメントは、3日以内に「Soduko TDDの大失敗」についてのコメントよりもGoogle検索で高く表示されていることに注意してください。それにもかかわらず、数独を解決しない方法はそれを要約しました:TDDは品質のためであり、正確さのためではありません。特にTDDでは、コーディングを開始する前にアルゴリズムを解決する必要があります。(私もコードファーストプログラマーではないというわけではありません。)
マークHurd

1
pvv.org/~oma/TDDinC_Yahtzee_27oct2011.pdfは興味深いかもしれません。

回答:


40

この質問にはすでに多くの良い答えがありますが、それらのいくつかについてコメントし、賛成しました。それでも、私はいくつかの考えを追加したいと思います。

柔軟性は初心者向けではありません

OPは、彼がTDDを経験していないことを明確に述べており、適切な答えはそれを考慮に入れる必要があると思います。スキル獲得のドレフュスモデルの用語では、彼はおそらく初心者です。初心者であることには何の問題もありません。新しいことを学び始めるとき、私たちはすべて初心者です。ただし、Dreyfusモデルが説明しているのは、初心者は

  • 教えられた規則や計画を厳守する
  • 裁量権を行使しない

それは人格の欠陥の説明ではないので、それを恥じる理由はありません-それは私たちすべてが新しい何かを学ぶために経験しなければならない段階です。

これはTDDにも当てはまります。

TDDは独断的である必要はなく、別の方法で作業する方が有益な場合があるという他の多くの回答にも同意しますが、それは始めたばかりの人には役立たないでしょう。経験がない場合、どのように裁量的な判断を下すことができますか?

初心者がTDDを行わなくてもよい場合があるというアドバイスを受け入れた場合、TDDを行わなくてもよい時期をどのように判断できますか?

経験も指導もないので、初心者ができることは、それが難しくなりすぎるたびにTDDをスキップすることだけです。それは人間の本性ですが、学ぶには良い方法ではありません。

テストを聞く

困難になったときにTDDをスキップすることは、TDDの最も重要な利点の1つを見逃すことです。テストは、SUTのAPIに関する早期のフィードバックを提供します。テストを書くのが難しい場合は、SUTが使いにくいという重要な兆候です。

これがGOOSの最も重要なメッセージの1つである理由です:テストを聞いてください!

この質問の場合、Yahtzeeゲームの提案されたAPIを見たときの私の最初の反応、およびこのページで見つけることができる組み合わせ論に関する議論は、これがAPIに関する重要なフィードバックであるということでした。

APIは、サイコロを整数の順序付きシーケンスとして表す必要がありますか?私にとって、その原始的強迫観念の匂い。だからこそ、私はRollクラスの導入を提案するtallsethからの答えを見てうれしかったです。それは素晴らしい提案だと思います。

しかし、その答えに対するコメントのいくつかは間違っていると思う。TDDが提案することは、Rollクラスが良いアイデアであるという考えを得ると、元のSUTの作業を中断し、RollクラスのTDDの作業を開始するということです。

TDDは包括的なテストを目的とするよりも「ハッピーパス」を目的としていることに同意しますが、それでもシステムを管理可能な単位に分割するのに役立ちます。Rollあなたがはるかに簡単に完了することができTDD何かのようなクラス・サウンド。

次に、Rollクラスが十分に進化したら、元のSUTに戻り、Roll入力の観点から肉付けします。

テストヘルパーの提案は、必ずしもランダム性を意味するものではありません。テストを読みやすくするための単なる方法です。

Rollインスタンスの観点から入力にアプローチしてモデル化する別の方法は、テストデータビルダーを導入することです。

Red / Green / Refactorは3段階のプロセスです

(TDDで十分な経験がある場合)一般的な感情に同意しますが、TDDに厳密に固執する必要はありません。ヤッツィーのエクササイズの場合、それはかなり貧弱なアドバイスだと思います。ヤッツィーのルールの詳細はわかりませんが、ここでは、Red / Green / Refactorプロセスに厳密に固執し、それでも適切な結果に到達できないという説得力のある議論はありません。

ここでほとんどの人が忘れているように見えるのは、Red / Green / Refactorプロセスの第3段階です。最初にテストを書きます。次に、すべてのテストに合格する最も単純な実装を作成します。次に、リファクタリングします。

この第3の状態では、すべての専門スキルを発揮できます。ここで、コードを検討することができます。

ただし、「完全に頭がおかしくなく、明らかに正しく動作しない、可能な限り単純なものを書く」だけでよいと述べるのはコツだと思います。事前に実装について十分に理解している(考えている)場合、完全なソリューション以外のすべて明らかに正しくなくなります。アドバイスに関して言えば、これは初心者にとってはまったく役に立ちません。

本当に起こるべきことは、明らかに正しくない実装ですべてのテストに合格できる場合、それは別のテストを書くべきであるというフィードバックです

これを行うと、最初に考えていた実装とはまったく異なる実装にどれほど頻繁につながるかは驚くべきことです。時々、そのように成長する代替案は、元の計画よりも優れている場合があります。

リゴールは学習ツールです

学習している限り、Red / Green / Refactorのような厳密なプロセスに固執することは非常に理にかなっています。学習者は、TDDが簡単なときだけでなく、難しいときにも経験を積む必要があります。

すべてのハードパーツを習得した場合にのみ、いつ「真の」パスから逸脱するかについて、十分な情報に基づいた決定を下すことができます。それはあなたがあなた自身の道を形成し始めるときです。


「ここにTDD初心者はいません。試してみることについての通常の不安もあります。明らかに正しくない実装ですべてのテストに合格できる場合は興味深いことになります。それは、別のテストを作成する必要があるというフィードバックです。「ブレインデッド」実装のテストは不必要な忙しい仕事であるという認識に取り組む良い方法のようです。
シャンブレーター14年

1
わぁ、ありがとう。私は、TDD(またはあらゆる分野)の初心者に「ルールを心配するのではなく、最高の気分でやる」と言う傾向があることに本当に怖いです。知識も経験もないときに、一番気持ちがいいものをどうやって知ることができますか?また、変換の優先順位の原則についても言及したいと思います。または、テストがより具体的になるにつれて、コードはより汎用的になるはずです。おじさんボブのような最も頑固なTDDサポーターは、「すべてのテストに新しいifステートメントを追加する」という概念を支持しません。
サラ

41

免責事項として、これは私が実践しているTDDであり、Kilianが適切に指摘しているように、私はそれを実践する正しい方法があると示唆した人には警戒します。しかし、多分それはあなたを助けるでしょう...

まず、テストに合格するためにできる最も簡単なことは次のとおりです。

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

これは、TDDの慣習によるものではなく、これらすべてのリテラルのハードコード化はあまり良いアイデアではないため、重要です。TDDで頭を包むのが最も難しいことの1つは、包括的なテスト戦略ではないことです。これは、コードをシンプルに保ちながら、回帰を防ぎ、進行状況をマークする方法です。これは開発戦略であり、テスト戦略ではありません。

この区別を述べる理由は、どのテストを書くべきかをガイドするのに役立つからです。「どのテストを書くべきか」に対する答え。「必要なコードを取得するために必要なテストは何でも」です。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);
}

ここで、意味ある 2つのテストケースを区別する方法を考え出す必要があります。個人的には、「テストをパスするために最も簡単なことを行う」ことと、「実装を促進するテストをパスするために最も簡単なことを行う」ことについて少し明確な情報を付け加えます。失敗したテストを書くことは、コードを変更する口実であるため、各テストを書くときは、「自分のコードが何をしたいのか、自分のやりたいことをどのように公開できるのか」を自問する必要があります。また、コードを堅牢にし、エッジケースを処理するのに役立ちます。発信者がナンセンスを入力したらどうしますか?

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テストケースを記述します。この場合、テストで欠陥を文書化し、合格します。

更新:

これはあなたの更新に対するコメントとして開始しましたが、かなり長くなり始めました...

問題は、リテラルの存在、ピリオドではなく、5つの部分からなる条件付きの「最も単純な」ものだと思います。考えてみると、5部構成の条件は実際にはかなり複雑です。赤から緑へのステップでリテラルを使用してから、リファクタリングステップで定数に抽象化するか、後のテストでリテラルを一般化するのが一般的です。

TDDでの自分の旅の中で、重要な違いがあることに気付きました。「単純な」と「鈍い」を混同するのは良くありません。つまり、私が始めたとき、私は人々がTDDをするのを見て、「彼らはただテストをパスするために可能な限り愚かなことをしているだけだ」と思った。 「鈍い」より。時には重なりますが、しばしば重なりません。

したがって、リテラルの存在が問題であるという印象を与えた場合は謝罪しますが、そうではありません。5つの節を含む条件の複雑さが問題だと思います。あなたの最初の赤から緑は、それが本当に単純であるため(そして偶然にも鈍いため)、「真を返す」ことができます。(1、2、3、4、5)の次のテストケースではfalseを返す必要があります。これは、「鈍角」を残し始める場所です。「(1、1、1、1、2、2)フルハウスで、(1、2、3、4、5)ではないのはなぜですか」と自問する必要があります。考えられる最も簡単なことは、1つが最後のシーケンス要素5または2番目のシーケンス要素2を持ち、もう1つが持っていないことです。これらはシンプルですが、(不必要に)鈍角でもあります。あなたが本当に運転したいのは、「同じ数の車がいくつあるか」です。そのため、繰り返しがあるかどうかを確認することで、2番目のテストに合格する場合があります。繰り返しのあるものでは、あなたは完全な家を持ち、もう一方ではそうではありません。これでテストに合格し、繰り返しはあるがアルゴリズムをさらに洗練するための完全な家ではない別のテストケースを作成します。

リテラルを使用してこれを実行する場合もしない場合もありますが、実行しても問題ありません。しかし、一般的な考え方は、ケースを追加するにつれてアルゴリズムを「有機的に」成長させることです。


質問を更新して、文字通りのアプローチから始めた理由に関する情報をさらに追加しました。
クリストフクレス

9
これは素晴らしい答えです。
tallseth

1
思慮深く説明された回答をありがとうございます。今私はそれについて考えることは実際に多くの意味をなします。
クリストフクレス

1
完全なテストとは、すべての組み合わせをテストすることではありません...それはばかげています。この特定のケースでは、特定のフルハウスまたは2つと非フルハウスのカップルを取ります。また、トラブルを引き起こす可能性のある特別な組み合わせ(つまり、5種類)。
シュライス

3
この答えの背後にある1つの原則は、ロバートC.マーティンの転換優先前提で記述されているcleancoder.posterous.com/the-transformation-priority-premise
マーク・シーマンを

5

特定の組み合わせで特定の5つのリテラル値をテストすることは、私の熱狂した脳にとって「最も単純」ではありません。問題を解決するには、本当に(あなたがちょうど3つのちょうど二つ持っているかどうかを数える明らかである場合には任意の値)を、その後、すべての手段によって、先に行くと、コードソリューションこと、及びで誤って満足することは非常に、非常に低いだろういくつかのテストを書きます書いたコードの量(つまり、異なるリテラルとトリプルとダブルの異なる順序)。

TDDの格言は実際にはツールであり、宗教的な信念ではありません。彼らのポイントは、あなたに正しい、十分にファクタリングされたコードを迅速に書かせることです。格言が明らかにそれを妨げている場合は、次のステップに進んでください。プロジェクトには、適用できる非自明な部分がたくさんあります。


5

エリックの答えは素晴らしいですが、私はテストライティングでトリックを共有するかもしれないと思いました。

このテストから始めます。

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

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

Roll5つのパラメーターを渡す代わりにクラスを作成すると、このテストはさらに改善されます。

[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);
}

その後、これ以上書く必要はないと思います(完全な家ではないと思うなら、2ペア、またはyahtzeeかもしれません)。

明らかに、Anyメソッドを実装して、あなたの基準を満たすランダムロールを返します。

このアプローチにはいくつかの利点があります。

  • 特定の値にとらわれないようにすることを唯一の目的とするテストを書く必要はありません。
  • テストはあなたの意図を本当にうまく伝えます(最初のテストのコードは「すべてのフルハウスがtrueを返します」と叫ぶ)
  • 問題の要点をすぐに理解できるようになります
  • 時々、あなたが考えていなかったケースに気付くでしょう

このアプローチを行う場合は、Assert.Thatステートメントのログメッセージを改善する必要があります。開発者は、どの入力が障害を引き起こしたかを確認する必要があります。
Bringer128

これは、鶏と卵のジレンマを生み出しませんか?AnyFullHouseを実装する場合(TDDも使用)、IsFullHouseが正しいことを確認する必要はありませんか?特に、AnyFullHouseにバグがある場合、そのバグはIsFullHouseで複製できます。
ワックスウィング

AnyFullHouse()は、テストケースのメソッドです。通常、テストケースはTDDですか?いいえ。また、フルハウス(または他のロール)のランダムな見本を作成する方が、その存在をテストするよりもはるかに簡単です。もちろん、テストにバグがある場合は、実稼働コードで複製できます。しかし、それはすべてのテストに当てはまります。
-tallseth

AnyFullHouseは、テストケースの「ヘルパー」メソッドです。それらが一般的であれば、ヘルパーメソッドも十分にテストされます!
マークハード

IsFullHouse本当に本当に返さtrueれるべきpairNum == trioNum ですか?
recursion.ninja

2

これをテストする際に検討する主な2つの方法を考えることができます。

  1. 有効なフルハウスセットのテストケース(〜5)をさらに追加し、同じ量の予想される偽({1、1、2、3、3}が適切なものです。誤った実装により、「同じプラス3ペア」として認識されます)。このメソッドは、開発者が単にテストに合格しようとしているだけでなく、実際に正しく実装していることを前提としています。

  2. 可能性のあるすべてのサイコロのセットをテストします(252種類しかありません)。もちろん、これは、予想される答えが何かを知る方法があることを前提としています(テストでは、これはoracle。として知られています)。これは、同じ機能の参照実装または人間である可能性があります。本当に厳密になりたい場合は、予想される各結果を手動でコーディングする価値があります。

たまたまヤッツィーのAIを書いたことがありますが、もちろんルールを知っていなければなりませんでした。スコア評価部分のコードはこちらで見つけることができます。実装はスカンジナビアバージョン(ヤッツィー)向けであり、実装はサイコロがソートされた順序で与えられることを前提としています。


百万ドルの問題は、純粋なTDDを使用してYahtzee AIを導き出しましたか?私の賭けはあなたができないということです。定義から盲目ではないドメイン知識を使用する必要があります:)
Andres F.

ええ、あなたは正しいと思います。これはTDDの一般的な問題です。予期しないクラッシュと未処理の例外のみをテストする場合を除き、テストケースには期待される出力が必要です。
ansjob

0

この例では、ポイントを実際に見逃しています。ここでは、ソフトウェア設計ではなく、単一の単純な機能について説明しています。少し複雑ですか?はい、あなたはそれを分解します。そして、1、1、1、1、1から6、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;

有効なヤッツィーロールではない入力を受け取った場合は、明日がないように投げるべきです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.