16

単体テストはホワイト ボックス テストであるため、コードで処理する必要があるすべてのケース、コードで処理する必要があるすべてのクライアント オブジェクト (テストではモック オブジェクト)、およびその正しい順序を事前に知っておく必要があると想定しています。クライアント オブジェクトはコードに含まれている必要があります (単体テストではモック オブジェクトの呼び出しが考慮されているため)。つまり、コードの詳細なアルゴリズムを正確に知っている必要があります。コードのアルゴリズムを正確に知る前に、まずそれを書かなければなりません!

私の観点からは、ソース コードを記述する前に正しい単体テストを記述する方法がわかりません。それでも、機能テストは一種のユーザー要件であるため、最初に機能テストを作成することは可能です。あなたのアドバイスは?宜しくお願いします

この質問の例を示します:
オブジェクトの依存関係がある場合、ソース コードを記述する前にテスト コードを記述する方法は?

4

5 に答える 5

28

言い換えれば、コードの詳細なアルゴリズムを正確に知っている必要があります。

完全ではありません。コード自体の外部から観察されるように、コードの詳細な動作を正確に知る必要があります。この動作を実現するアルゴリズム、またはアルゴリズムの組み合わせ、または任意のレベルの抽象化/ネスト/計算など。テストには重要ではありません。テストは、目的の結果が達成されることだけを考慮します。

したがって、テストの価値は、コードがどのように動作するかを指定することです。したがって、テストに対して検証できる限り、コードは必要なものをすべて自由に変更できます。パフォーマンスの向上、読みやすさとサポート性のリファクタリングなどを行うことができます。テストにより、動作が変更されないことが確認されます。

たとえば、2つの数値を加算する関数を作成するとします。頭の中でそれをどのように実装するかを知っているかもしれませんが、その知識はしばらく脇に置いておきます。まだ実装していません。まず、テストを実装しています...

public void CanAddIntegers()
{
    var addend = 1;
    var augend = 1;
    var result = MyMathObject.Add(addend, augend);
    Assert.AreEqual(2, result);
}

テストが完了したので、メソッドを実装できます...

public int Add(int addend, int augend)
{
    return ((addend * 2) + (augend * 2)) / 2;
}

うわあ。ちょっと待ってください...なぜ私はそれをそのように実装したのですか?さて、テストの観点から、誰が気にしますか?合格です。実装は要件を満たしています。テストが完了したので、コードを安全にリファクタリングできます...

public int Add(int addend, int augend)
{
    return addend + augend;
}

それはもう少し正気です。そして、テストはまだ合格です。実際、コードをさらに減らすことができます...

public int Add(int addend, int augend)
{
    return 2;
}

何だと思う?テストはまだ合格です。これは私たちが持っている唯一のテストであり、与えられた唯一の仕様なので、コードは「機能します」。したがって、より多くのケースをカバーするために、明らかにテストを改善する必要があります。より多くのテストを作成すると、より多くのコードを作成するために必要な仕様が得られます。

実際、 TDDの3番目のルールによれば、その最後の実装は最初の実装である必要があります。

1つの失敗した単体テストに合格するのに十分な数を超える本番コードを作成することは許可されていません。

したがって、純粋にUncle-Bob主導のTDDの世界では、最初にその最後の実装を記述し、次にさらにテストを記述して、コードを徐々に改善していました。

これは、赤、緑、リファクタリングサイクルとして知られています。これは、少し工夫が凝らされていない単純な例であるボウリングゲームで非常によく示されています。その演習の目的は、そのサイクルを練習することです。

  1. まず、特定の動作を期待するテストを作成します。これはサイクルの赤い部分です。これは、適切な動作がないとテストが失敗するためです。
  2. 次に、その動作を示すコードを記述します。その目的はテストに合格することであるため、これはサイクルのグリーン部分です。そして、テストに合格するためだけに。
  3. 最後に、コードをリファクタリングして改善します。これは、当然、サイクルのリファクタリング部分です。

行き詰まっているのは、サイクルのリファクタリング部分に永続的にいるということです。あなたはすでにコードをより良くする方法について考えています。どのアルゴリズムが正しいか、それをどのように最適化するか、最終的にどのように書くべきか。そのために、TDDは忍耐力の練習です。最高のコードを書かないでください...まだ

  1. まず、コードが何をすべきかを決定します。それ以上は何もしません。
  2. 次に、それを実行するコードを記述します。それ以上は何もしません。
  3. 最後に、そのコードを改善し、改善します。

アップデート

この質問を思い出させる何かに出くわしましたが、ランダムに思い浮かびました。おそらく私はあなたが求めていることの状況を誤解しました。依存関係をどのように管理していますか?つまり、どのような依存性注入手法を使用していますか?それがここで議論されている問題の根源かもしれないように思えます。

私が覚えている限り、私はCommon Service Locator(または、より一般的には、同じ概念の自家製の実装)のようなものを使用してきました。そしてそうすることで、私は依存性注入の非常に特殊なスタイルに向かう傾向があります。別のスタイルを使用しているようです。コンストラクター注入、おそらく?この答えのために、コンストラクターインジェクションを想定します。

MyMathObjectそれで、あなたが示すように、それがとに依存しているMyOtherClass1としましょうMyOtherClass2。コンストラクタインジェクションを使用すると、のフットプリントは次のMyMathObjectようになります。

public class MyMathObject
{
    public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
    {
        // implementation details
    }

    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

したがって、ご指摘のとおり、テストでは依存関係またはそのモックを提供する必要があります。クラスのフットプリントには、またはの実際の使用の兆候はありませんが、それらの必要性の兆候はあります。依存関係として、それらはコンストラクターによって大声でアドバタイズされます。MyOtherClass1MyOtherClass2

だから、これはあなたが尋ねた質問を懇願します...オブジェクトをまだ実装していないときに、最初にテストを書くにはどうすればよいですか?繰り返しになりますが、オブジェクトの外向きのデザインだけで実際の使用を示すものはありません。したがって、依存関係は、知っておく必要のある実装の詳細です。

それ以外の場合は、最初にこれを記述します。

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

次に、テストを作成し、それを実装して依存関係を発見し、テストを再作成します。そこに問題があります。

ただし、あなたが見つけた問題は、テストやテスト駆動開発の問題ではありません。問題は実際にはオブジェクトのデザインにあります。釉薬がかけられているという事実にもかかわらず、// implementation detailsまだ逃げている実装の詳細があります。漏れのある抽象化があります:

public class MyMathObject
{
    public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
    {                   ^---Right here                 ^---And here

        // implementation details
    }

    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

オブジェクトは、実装の詳細を十分にカプセル化および抽象化していない。それは試みており、依存性注入の使用はそれに向けた大きな一歩です。しかし、まだ完全にはありません。これは、実装の詳細である依存関係が外部から見え、他のオブジェクトによって外部から認識されているためです。(この場合、テストオブジェクトです。)したがって、依存関係を満たし、機能させるにはMyMathObject、外部オブジェクトがその実装の詳細を知る必要があります。それらはすべてそうです。テストオブジェクト、それを使用するプロダクションコードオブジェクト、それに依存するあらゆるもの。

そのためには、依存関係の管理方法を切り替えることを検討することをお勧めします。コンストラクターインジェクションやセッターインジェクションのようなものの代わりに、依存関係の管理をさらに反転させ、オブジェクトにさらに別のオブジェクトを介してそれらを内部的に解決させます。

前述のサービスロケーターを開始パターンとして使用すると、依存関係を解決することが唯一の目的(単一責任)であるオブジェクトを作成するのは非常に簡単です。依存性注入フレームワークを使用している場合、このオブジェクトは通常、フレームワークの機能の単なるパススルーです(ただし、フレームワーク自体を抽象化するため、依存性が1つ少なくなります。これは良いことです)。自家製の機能を使用する場合、このオブジェクトはその機能を抽象化します。

しかし、最終的には次のようなものになりますMyMathObject

private SomeInternalFunction()
{
    var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
    // implementation details
}

したがってMyMathObject、依存性注入を使用した場合でも、のフットプリントは次のようになります。

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

リークのある抽象化や外部的に知られている依存関係はありません。実装の詳細が変更されたため、テストを変更する必要はありません。これは、テスト対象のオブジェクトからテストを分離するためのもう1つのステップです。

于 2013-01-26T14:27:46.320 に答える
2

「ソフトウェア」の意図が何であるかを理解していない場合、テストを作成できないことは明らかですが、要件または仕様が詳細に記述されていれば、テストを作成することは絶対に可能です。

失敗する要件に適合するものを書くことができます。次に、仕様に基づいてテストに合格するための最小限の作業量を生成します。

あなたがある種の天才でない限り、この最初のカットでは、リファクタリングと抽象化の導入、パターン、保守性、パフォーマンス、およびその他のあらゆる種類のことを説明する必要があります。

したがって、要件が理解されていれば、最初にテストできますが、実装がまとまってテストに合格するための実装のみが必要になるまで、テストは合格しません。

このように動作することは、実際には常に法案に適合するとは限りません。特に、仕様を取得するのが難しい場合は特にそうです。開発者として必要なものが得られない場合は、やみくもにこの道を突っ込まないように注意する必要があります。また、コードを継承したり、「ブラウンフィールド」プロジェクトに追加したりするときにも、達成できないことがよくあります。開発者として、早い段階で実用性を検討することが重要です。

于 2013-01-26T14:22:13.580 に答える
1

まず第一に、単体テストの多くはモックを必要とせず、他のオブジェクトとの相互作用も必要としません。あなたの質問はそれらには当てはまりません。

新しいメソッドの目的、または目的の一部が、連携するオブジェクトに影響を与えることである場合、それはテストする必要があるものの一部です。それが目的ではないが、実装が共同作業者に偶発的に影響を与える場合は、その偶発的な影響をテストするべきではありません。

いずれにせよ、コードを書く前にテストを書くことができることは簡単にわかります。メソッドが別のオブジェクトに影響を与えると想定されている場合、テストはそのように言う必要があります。メソッドが想定されていない場合、テストはメソッドと他のオブジェクトとの相互作用について何も言う必要はありません。

于 2013-01-26T17:10:02.110 に答える
0

これは、TDD を開始するときに乗り越えるのは非常に難しい問題です。SO、特にprogrammers.stackexchange.comには多くの良い答えがあると確信しています(この質問はおそらくそのフォーラムに適しています)。

理解を助けるために私が言えることはたくさんありますが、実際に TDD を実行するほど効果的なものはありません。手始めに、コード katas に関するこの記事を参照してください。この記事は、いくつかの便利な TDD 演習にリンクしています。

于 2013-01-26T14:22:55.567 に答える
0

そうではありません。TDD では、製品コードで処理することを期待しているテスト ケースから始めます。製品コードがなくてもよいというわけではありません。クラス関数などが存在する可能性があります。失敗したテスト ケースから始めて、合格するように製品コードに変更を加え続けます。

http://msdn.microsoft.com/en-us/library/aa730844(v=vs.80).aspx#guidelinesfortdd_topic2を参照してください。

于 2013-01-26T14:23:23.800 に答える