単体テストに合格するための最小限のコードを書く-不正行為なし!


36

TDDを実行して単体テストを作成する場合、テストする「実装」コードの最初の反復を作成するときに、「チート」という衝動にどのように抵抗しますか。

例:
数値の階乗を計算する必要があります。次のような単体テスト(MSTestを使用)から始めます。

[TestClass]
public class CalculateFactorialTests
{
    [TestMethod]
    public void CalculateFactorial_5_input_returns_120()
    {
        // Arrange
        var myMath = new MyMath();
        // Act
        long output = myMath.CalculateFactorial(5);
        // Assert
        Assert.AreEqual(120, output);
    }
}

このコードを実行すると、CalculateFactorialメソッドが存在しないため失敗します。そこで、テスト対象のメソッドを実装するためのコードの最初の反復を記述し、テストに合格するために必要な最小限の コードを記述します

実は、私は次のように書き続けたいと思っています。

public class MyMath
{
    public long CalculateFactorial(long input)
    {
        return 120;
    }
}

これは、技術的には、それで正しいです、本当に最低限のコードをするために必要とされている特定のテストパス作るそれは本当にさえいないので、それは明確に「チート」ですが、(緑行く)を試みる階乗を計算する機能を実行します。もちろん、今では、リファクタリングの部分は、実装の真のリファクタリングではなく、「正しい機能の記述」の演習になります。明らかに、異なるパラメーターで追加のテストを追加すると失敗し、リファクタリングが強制されますが、その1つのテストから開始する必要があります。

だから、私の質問は、「テストに合格するための最小限のコードを書く」ことと、機能を維持しつつ、実際に達成しようとしていることの精神とのバランスをどのように得るかということです。


4
それは人間のことです。カンニングをする衝動に抵抗しなければなりません。それ以上のものはありません。テストするコードよりも多くのテストを追加し、より多くのテストコードを記述することもできますが、その贅沢さがなければ、ただ抵抗する必要があります。コーディングには、ハッキングやチートの衝動に抵抗しなければならない場所がたくさんあります。なぜなら、今日では機能するかもしれませんが、後で機能しないことを知っているからです。
ダンローゼンスターク

7
確かに、TDDでは、逆にそれを行うことは不正です。つまり、リターン120が適切な方法です。自分でそれをやらせるのは非常に難しく、先を争って階乗計算を書き始めるのは難しいです。
ポール・ブッチャー

2
テストに合格する可能性があるが、実際の機能を追加したり、問題の最終的な解決策に近づけたりしないため、これをチートと見なします。
-GrumpyMonkey

3
クライアントコードコードが5を渡すだけであることが判明した場合、120を返すことは単なる非チートではなく、実際には正当なソリューションです。
Kramii復活モニカ

@PaulButcherに同意します。実際、テキストや記事の多くの単体テストの例では、このアプローチを採用しています。
ホルスコル

回答:


45

それは完全に合法です。赤、緑、リファクタリング。

最初のテストに合格しました。

新しい入力を使用して、2番目のテストを追加します。

すぐに緑色になり、if-elseを追加できます。これは正常に機能します。合格しましたが、まだ完了していません。

Red、Green、Refactorの3番目の部分が最も重要です。 重複を削除するためのリファクタリング。これで、コードに重複ができます。整数を返す2つのステートメント。 そして、その重複を取り除く唯一の方法は、関数を正しくコーディングすることです。

最初は正しく書かないでくださいと言っているのではありません。私はあなたがそうしないなら、それは不正行為ではないと言っています。


12
これは単に問題を提起するだけで、なぜ最初に関数を正しく記述しないのですか?
ロバートハーベイ

8
@Robert、階乗数は簡単です。TDDの本当の利点は、自明ではないライブラリを作成することです。テストを作成すると、実装前にAPIを設計する必要があります。これは-私の経験では-より良いコードにつながります。

1
@ロバート、テストに合格する代わりに問題を解決することを心配しているのはあなたです。些細でない問題については、テストを実施するまでハードデザインを延期する方が効果的であると言います。

1
@ThorbjørnRavn Andersen、いいえ、私はあなたがたった一度しか戻れないと言っているわけではありません。複数の正当な理由(ガードステートメントなど)があります。問題は、両方のreturnステートメントが「等しい」ことです。彼らは同じ「こと」をしました。それらはたまたま異なる値を持っています。TDDは剛性に関するものではなく、特定のサイズのテスト/コード比に準拠しています。それは、コードベース内に快適レベルを作成することです。失敗したテストを作成できる場合、その機能の将来のテストで機能する機能は素晴らしいです。それを行ってから、エッジケーステストを記述して、機能が引き続き機能することを確認します。
CaffGeek

3
完全な(単純ではあるが)実装を一度に記述しないことのポイントは、テストが失敗する可能性もまったくないということです。合格する前にテストが失敗するのは、コードへの変更がアサーションを満足させたことを実際に証明できるということです。これが、TDDが回帰テストスイートを構築するのに非常に優れており、その意味で「テスト後」アプローチで床を完全に一掃する唯一の理由です。
サラ

25

明らかに、最終目標の理解と、その目標を達成するアルゴリズムの達成が必要です。

TDDは設計の魔法の弾丸ではありません。コードを使用して問題を解決する方法をまだ知っている必要があり、テストパスを行うためにコードの数行よりも高いレベルでそれを行う方法を知っている必要があります。

TDDが良いデザインを促進するので、TDDのアイデアが好きです。テスト可能なようにコードを記述する方法を考えるようになります。一般的に、この哲学はコードを全体的に優れた設計に向けて推進します。ただし、ソリューションを設計する方法を知る必要があります。

私は、テストに合格するために最小限のコードを書くだけでアプリケーションを成長させることができると主張する還元主義TDD哲学を支持しません。アーキテクチャについて考えることなくこれは機能しません。あなたの例はそれを証明しています。

ボブ・マーティンおじさんはこう言います:

テスト駆動開発を行っていない場合、専門家と呼ぶことは非常に困難です。ジム・コプリンは、私をこのカーペットのために呼びました。彼は私がそれを言ったことを好きではなかった。実際、彼の現在の立場は、テスト駆動開発がアーキテクチャを破壊しているということです。人々は他の種類の思考の放棄にテストを書き、テストをパスするために狂ったように彼らのアーキテクチャを引き裂き、彼は興味深い点を持っているので、それは儀式を乱用し、規律の背後にある意図を失う興味深い方法です。

あなたがアーキテクチャを考えていない場合、代わりにあなたがしていることはアーキテクチャを無視し、テストを一緒に投げてそれらを通過させることである場合、あなたはそれが建物にとどまることを可能にするものを破壊していますシステムの構造と、システムが構造の完全性を維持するのに役立つ堅実な設計決定。

単にテストをまとめて投げて、10年ごとに10年ごとに合格させ、システムが生き残ると仮定することはできません。私たちは地獄に進化したくありません。そのため、優れたテスト駆動開発者は常に大局を考えて、アーキテクチャの決定を意識しています。


質問への答えではありませんが、1 +
誰も

2
@rmx:ええと、質問は、「テストに合格するための最小限のコードを書く」ことと、機能を維持しつつ、実際に達成しようとしていることの精神とのバランスをどのように得るかです。 同じ質問を読んでいますか?
ロバートハーベイ

理想的なソリューションはアルゴリズムであり、アーキテクチャとは関係ありません。TDDを実行しても、アルゴリズムを発明することにはなりません。ある時点で、アルゴリズム/ソリューションの観点から手順を実行する必要があります。
ジョッペ

@rmxに同意します。これは私の特定の質問それ自体に実際に答えているわけではありませんが、一般にTDDがソフトウェア開発プロセス全体の全体像にどのように適合するかについての思考の糧となります。そのため、+ 1。
CraigTP

「アーキテクチャ」の代わりに「アルゴリズム」やその他の用語を使用することができると思いますが、議論はまだ続いています。木のために木を見ることができないことがすべてです。整数入力ごとに個別のテストを作成する場合を除き、TDDは、適切な階乗の実装と、テストされたすべてのケースで機能するが他のケースでは機能しない一部のハードコーディングを区別できません。TDDの問題は、「すべてのテストに合格」と「コードが良好」が混同されやすいことです。ある時点で、常識の重い尺度を適用する必要があります。
ジュリアヘイワード14年

16

非常に良い質問です...そして、@ Robert以外のほとんどの人と意見を異にする必要があります。

書き込み

return 120;

階乗関数が1つのテストに合格するのは時間の無駄です。それは「ごまかし」ではなく、文字通りred-green-refactorに従ってもいません。それは間違っています。

その理由は次のとおりです。

  • 階乗の計算は機能であり、「定数を返す」のではありません。「return 120」は計算ではありません
  • 「リファクタリング」引数の見当違い。5と6の2つのテストケースがある場合、階乗をまったく計算していないため、このコードはまだ間違っています。

    if (input == 5) { return 120; } //input=5 case
    else { return 720; }   //input=6 case
    
  • 「リファクタリング」引数に文字通り従えば、5つのテストケースがある場合、YAGNIを呼び出し、ルックアップテーブルを使用して関数を実装します。

    if (factorialDictionary.Contains(input)) {
        return factorialDictionary[input]; 
    }
    throw new Exception("Input failure");
    

これらのどれも実際には何も計算していません。そして、それは仕事ではありません!


1
@rmx:いいえ、見逃していません。「重複を除去するリファクタリング」は、ルックアップテーブルで満たすことができます。ところで、単体テストが要件をエンコードするという原則はBDDに固有のものではなく、Agile / XPの一般的な原則です。要件が「5の階乗とは何か」という質問に答える場合、「120を返す」。合法だろう;-)
スティーブンA.ロウ

2
@Chadはすべて不必要な作業です-初めて関数を書くだけです;
スティーブンA.ロウ

2
@Steven A.Lowe、その論理によって、なぜテストを書くのですか?!「初めてアプリケーションを書くだけです!」TDDのポイントは、小さく、安全で、段階的な変更です。
CaffGeek

1
@チャド:ストローマン。
スティーブンA.ロウ

2
完全な(単純ではあるが)実装を一度に記述しないことのポイントは、テストが失敗する可能性もまったくないということです。合格する前にテストが失敗するのは、コードへの変更がアサーションを満足させたことを実際に証明できるということです。これが、TDDが回帰テストスイートを構築するのに非常に優れており、その意味で「テスト後」アプローチで床を完全に一掃する唯一の理由です。失敗しないテストを誤って作成することはありません。また、叔父のボブの素因数カタを見てみましょう。
サラ

10

単体テストを1つだけ記述した場合、1行の実装(return 120;)は正当です。120の値を計算するループを書く- それは不正行為になるでしょう!

このような簡単な初期テストは、エッジケースを検出して1回限りのエラーを防ぐのに適した方法です。実際には、5は最初に入力する値ではありません。

ここで役に立つかもしれない経験則は、ゼロ、1、多く、ロットです。0と1は、階乗の重要なエッジケースです。ワンライナーで実装できます。「多くの」テストケース(5!など)は、ループの作成を強制します。「ロット」(1000 !?)テストケースでは、非常に大きな数を処理するための代替アルゴリズムを実装する必要があります。


2
「-1」の場合は興味深いでしょう。明確に定義されていないため、テストを作成する人とコードを作成する人の両方が、何起こるべきかを最初に同意する必要があります。
gnasher729

2
factorial(5)悪い最初のテストであると実際に指摘するために+1 。最も単純なケースから開始し、各反復でテストをもう少し具体的にし、コードをもう少し汎用的にするように促します。これは叔父ボブは変換優先前提と呼ぶものである(blog.8thlight.com/uncle-bob/2013/05/27/...
サラ

5

テストが1つしかない限り、テストに合格するために必要な最小限のコードはreturn 120;であり、テストがない限り、そのために簡単に保持できます。

これにより、このメソッドのその他の戻り値を実行するテストを実際に記述するまで、さらに設計を延期できます。

テストは仕様の実行可能なバージョンであり、その仕様がすべてf(6)= 120ということだけであれば、それは法案に完全に適合することを覚えておいてください。


マジ?このロジックにより、誰かが新しい入力を思いつくたびにコードを書き直す必要があります。
ロバートハーヴェイ

6
@Robert、いくつかの時点で新しいケースを追加しても、可能限り単純なコードは作成されず、その時点で新しい実装を作成します。テストが既に実施されているので、新しい実装が古い実装と同じタイミングで実行されることを正確に把握できます。

1
@ThorbjørnRavn Andersenは、まさにRed-Green-Refactorの最も重要な部分であり、リファクタリングです。
-CaffGeek

+1:これは私の知識からの一般的な考え方でもありますが、暗黙の契約を履行することについて何か言う必要があります(すなわち、メソッド名factorial)。f(6)= 120だけを指定(テスト)する場合、「120を返す」だけで十分です。f(x)== x * x-1 ... * xx-1:upperBound> = x> = 0であることを確認するテストの追加を開始すると、階乗方程式を満たす関数に到達します。
スティーブンエバーズ

1
@SnOrfus、「暗黙の契約」が存在する場所はテストケースです。契約が階乗の場合、既知の階乗がそうであるかどうか、そして既知の非階乗がそうでないかどうかをテストします。たくさんあります。最初の10個の階乗のリストを、10番目までの階乗までのすべての数値をforループテストに変換するのに時間がかかりません。

4

このような方法で「チート」できる場合は、単体テストに欠陥があることを示唆しています。

1つの値で階乗法をテストするのではなく、値の範囲でテストします。ここでは、データ駆動型テストが役立ちます。

ユニットテストを要件の明示として表示します。テストするメソッドの動作をまとめて定義する必要があります。(これは行動駆動開発として知られています -その未来;-)

だから、自問してみてください-誰かが実装を間違ったものに変更したとしても、あなたのテストはまだパスするでしょうか、それとも「ちょっと待って!」

それを念頭に置いて、あなたの唯一のテストが質問のテストであった場合、技術的には、対応する実装は正しいです。この問題は、定義が不十分な要件と見なされます。


nandaが指摘したように、いつでも無限の一連のcaseステートメントをa switchに追加することができ、OPの例のすべての可能な入力と出力のテストを書くことはできません。
ロバートハーヴェイ

からInt64.MinValueまでの値を技術的にテストできますInt64.MaxValue。実行には長い時間がかかりますが、エラーの余地なく要件を明示的に定義します。現在の技術では、これは実行不可能です(将来的にはより一般的になる可能性があると思います)、あなたはカンニングすることができますが、OPの質問は実用的なものではないと思います(誰も実際にカンニングしないでしょう)実際には)、しかし理論的なもの。
誰も

@rmx:それができれば、テストはアルゴリズムになり、アルゴリズムを記述する必要はなくなります。
ロバートハーベイ

それは本当です。私の大学の論文では実際に、TDDの補助として遺伝的アルゴリズムを使用して、単体テストをガイドとして使用した実装の自動生成が行われています。違いは、通常、要件をコードにバインドすることは、単体テストを具体化する単一の方法よりも読み取りや把握がはるかに難しいことです。次に質問があります:実装が単体テストの明示であり、単体テストが要件の明示である場合、テストを完全にスキップしないのはなぜですか?答えがありません。
誰も

また、私たち人間は、実装コードと同じように、単体テストでも間違いを犯す可能性が高いのではないでしょうか?それでは、なぜ単体テストなのでしょうか?
誰も

3

さらにテストを書くだけです。最終的には短くなります書くのなります

public long CalculateFactorial(long input)
{
    return input <= 1 ? 1 : CalculateFactorial(input-1)*input;
}

より

public long CalculateFactorial(long input)
{
    switch (input) {
       case 0: return 1;
       case 1: return 1;
       case 2: return 2;
       case 3: return 6;
       case 4: return 24;
       case 5: return 120;
    }
}

:-)


3
そもそもアルゴリズムを正しく書くだけじゃないの?
ロバートハーベイ

3
@ロバート、それは0から5までの数の階乗を計算するため正しいアルゴリズムです。さらに、「正しく」とはどういう意味ですか?これは非常に単純な例ですが、より複雑になると、「正しい」という意味の多くのグラデーションが生じます。ルートアクセスを必要とするプログラムは十分に「正しい」ですか?CSVを使用する代わりに、XMLを「正しい」使用していますか?これに答えることはできません。TDDでテストとして定式化されたいくつかのビジネス要件を満たす限り、どのアルゴリズムも正しいです。
P Shved

3
出力タイプが長いため、関数が正しく処理できる入力値はわずか(20程度)しかないため、大きなswitchステートメントは必ずしも最悪の実装ではないことに注意してください-速度がもっと高い場合コードサイズよりも重要であるため、優先順位によってはswitchステートメントを使用する方法があります。
user281377

3

「OK」の値が十分に小さい場合、「チート」テストの作成は問題ありません。ただし、リコール単位テストは、すべてのテストに合格した場合にのみ完了し、失敗する新しいテストを作成することはできません。大量のifステートメント(またはさらに良いのは大きなswitch / caseステートメント:-) を含むCalculateFactorialメソッドが本当に必要な場合は、それを行うことができます。また、固定精度の数値を扱うために必要なコードこれを実装するのは有限です(おそらくかなり大きくていですが、おそらくプロシージャのコードの最大サイズに関するコンパイラまたはシステムの制限によって制限されます)。この時点で本当にすべての開発は、あなたがすべてのブランチ以下によって達成することができるものよりも短い時間の量で結果を計算するためのコードを必要とし、テスト書くことができますユニットテストによって駆動されなければならないことを主張するかの声明を。

基本的に、TDDは要件を正しく実装するコード書くのに役立ちますが、良いコードを書くことを強制することはできません。それはあなた次第です。

共有してお楽しみください。


「ユニットテストはすべてのテストに合格した場合にのみ完了し、失敗する新しいテストを作成できない」ための+1全体的な要件が特定のケースのみを必要とする場合」
-Thymine

1

ここでのRobert Harveysの提案には100%賛成です。テストに合格するだけでなく、全体的な目標にも留意する必要があります。

「入力の特定のセットで動作することが検証されている」というあなたの痛みの解決策として、xunit理論などのデータ駆動型テストを使用することを提案します。この概念の背後にある力は、入力から出力への仕様を簡単に作成できることです。

階乗の場合、テストは次のようになります。

    [Theory]
    [InlineData(0, 1)]
    [InlineData( 1, 1 )]
    [InlineData( 2, 2 )]
    [InlineData( 3, 6 )]
    [InlineData( 4, 24 )]
    public void Test_Factorial(int input, int expected)
    {
        int result = Factorial( input );
        Assert.Equal( result, expected);
    }

さらに、テストデータ提供を実装することもできます(それは IEnumerable<Tuple<xxx>>)を、数学的な不変式をエンコード。たとえば、nで繰り返し除算するとn-1が生成されます。

このtpは非常に強力なテスト方法であることがわかりました。


1

それでもチートできる場合は、テストだけでは十分ではありません。さらにテストを書く!あなたの例では、入力1、-1、-1000、0、10、200のテストを追加しようとします。

それでも、もしあなたが本当にカンニングをするなら、無限のif-thenを書くことができます。この場合、コードのレビュー以外には何も役に立ちません。あなたはすぐに受け入れテスト(他の人によって書かれました!

単体テストの問題は、プログラマーがそれらを不要な作業と見なすことです。それらを見る正しい方法は、あなたの仕事の結果を正しくするためのツールとしてです。したがって、if-thenを作成する場合、考慮すべき他のケースがあることを無意識に知っています。つまり、別のテストを作成する必要があります。不正行為が機能していないことに気付くまで続き、正しい方法でコーディングする方が良いでしょう。まだ終了していないと感じる場合は、終了していません。


1
したがって、テストに合格するだけのコードを(TDD支持者として)書くだけでは不十分であると言っているように思えます。また、健全なソフトウェア設計の原則に留意する必要があります。ところで私はあなたに同意します。
ロバートハーベイ

0

テストの選択は最良のテストではないことをお勧めします。

私は次のことから始めます:

最初のテストとしてfactorial(1)

2番目としてfactorial(0)

3番目としてfactorial(-ve)

そして、自明ではないケースを続けます

そして、オーバーフローケースで終わります。


なに-ve
ロバートハーベイ

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