TDDを使用する正しい方法がどれなのか混乱しています


8

TDDの背後にあるアイデアと、チームがTDDを使用する方法を把握しようとしています。NUnit + Moqを使用した次のテストケースがあります(メモリで書き込むだけで、サンプルがコンパイルされるとは限りませんが、説明が必要です)。

[Test]
public void WhenUserLogsCorrectlyIsRedirectedToLoginCorrectView() {
    Mock<IUserDatabaseRepository> repoMock = new Mock<IUserDatabaseRepository>();
    repoMock.Setup(m => m.GetUser(It.IsAny())).Returns(new User { Name = "Peter" });        

    Mock<ILoginHelper> loginHelperMock = new Mock<ILoginHelper>();
    loginHelperMock.Setup(m => m.Login(It.IsAny(), It.IsAny())).Returns(true);
    Mock<IViewModelFactory> factoryMock = new Mock<IViewModelFactory>();
    factoryMock.Setup(m => m.CreateViewModel()).Returns(new LoginViewModel());

    AccountController controller = new AccountController(repoMock.Object, loginHelperMock.Object, factoryMock.Object)

    var result = controller.Index(username : "Peter", password: "whatever");

    Assert.AreEqual(result.Model.Username, "Peter");
}

AccountControllerには3つの依存関係があります。これを模擬しています。コントローラー内で調整すると、ログインが正しいかどうかを確認できます。

私の心をつまずくのは... TDDで理論的に最初にテストスイートを記述し、そこからコードをビルドする必要がある場合、操作を実行するために使用する必要があることを事前に知っておくにはどうすればよいですか。これらの3つの依存関係と、操作によって特定の操作が呼び出されることはありますか?それは、依存関係をモックしてクラスを分離するために実装する前に、テスト中のサブジェクトの内部を知る必要があるようなもので、ある種の書き込みテスト-コードの作成-必要に応じてテストを変更します。

当然、コードの内部についての知識がなく、テストを表現するだけで、ILoginHelperが必要であるように表現でき、「魔法のように」コードを書く前に、成功したログインでユーザーを返すと(そして最終的には)基礎となるフレームワークがそのように機能しないことを認識してください(たとえば、完全なオブジェクトの代わりにIDだけを返す)。

TDDを間違った方法で理解していますか?複雑なケースでの典型的なTDDプラクティスはどれですか?

ありがとうございました


1
単体テストのメリットを最大限に活用するために、厳密なTDDに従う必要はありません。
2014年

2
@Den:「厳密なTDD」は、OPがそれを意味すると信じていることを意味するものではありません。
Doc Brown、

私はvimeo.com/album/3143213/video/71816368(8LU:TDDの Advanced Concepts)を視聴することをお勧めします。それはあなたが物事の周りにあなたの頭を得るのを助けるかもしれません。
Andrew Eddie

回答:


19

TDDで理論的には、最初にテストスーツを作成し、それからコードをビルドする必要がある場合

ここにあなたの誤解があります。TDDは最初に完全なテストスイートを作成することではありません。それは誤った神話です。TDDは小さなサイクルで作業することを意味し、

  • 一度に1つのテストを書く
  • テストを「グリーン」にするために必要なだけのコードを実装する
  • リファクタリング(コードテスト)

したがって、テストスイートの作成は1つのステップで行われるのではなく、「コードが記述される前」に行われるのではなく、問題のコードの実装と織り交ぜられます。

例に適用:依存関係のないコントローラ(プロトタイプのようなもの)の簡単なテストから始める必要があります。次に、コントローラーを実装し、リファクタリングします。その後、コントローラーがもう少し行うことを期待する新しいテストを追加するか、既存のテストをリファクタリング/拡張します。次に、新しいテストが「緑」になるまでコントローラーを変更します。このようにして、テストとテスト対象の単純な組み合わせから始め、複雑なテストとテスト対象に終わります。

このルートをたどると、ある時点で、コントローラーがその作業を行うための入力として必要な追加データがわかります。これは確かに、次のテストを設計するときではなく、コントローラーメソッドを実装しようとしたときに発生する可能性があります。これは、短時間でメソッドを実装するのをやめ、不足している依存関係の導入を最初に開始するポイントです(おそらく、コントローラーのコンストラクターのリファクタリングによる)。これにより、既存のテストのリファクタリングが簡単になります。TDDでは、通常、最初にコンストラクターを呼び出すテストを変更し、その後に新しいコンストラクター属性を追加します。そして、テストのコーディングと記述が完全に絡み合うところです。


13

私の心をつまずくのは... TDDで理論的に最初にテストスーツを記述し、そこからコードをビルドする必要がある場合、どのように私は私の操作を実行するために使用する必要があることを事前に知っているはずですか?これらの3つの依存関係と、操作によって特定の操作が呼び出されることはありますか?それは、依存関係をモックしてクラスを分離するために実装する前に、テスト中のサブジェクトの内部を知る必要があるようなもので、ある種の書き込みテスト-コードの作成-必要に応じてテストを変更します。

気分が悪いですよね?そして、それは-コントローラのテストが何らかの方法で不正確だったり「不良」だったりするのではなく、「制御」されるべきものがなくなる前にコントローラをテストしたいのです。:)

私の要点は、「ビジネスルール」と「実際のアプリケーションロジック」のレベルでTDDを使い始めると、TDDがより自然に感じるようになるということです。コントローラーは通常、他のコンポーネントへの委任のみを扱うため、委任が正しく行われたかどうかをテストするには、委任先のオブジェクトを知る必要があります。唯一の問題は、実際のロジックを実装する前に実行しようとした場合です。たとえば、TDDをより「動作指向」の方法で行うことで、LoginHelperの実装を試みることをお勧めします。それはより自然に感じるでしょう、そしてあなたはおそらくその利点のより多くを見るでしょう。

つまり、より一般的な答えとしては、TDDは必要なコードを記述する前にテストを作成する手法ですが、どの種類のテストを指定するのかということではありません。コントローラーは通常、コンポーネントのインテグレーターであるため、多くのモックを必要とする単体テストを作成します。アプリケーションロジック(注文、ユーザー認証の検証などのビジネスルールなど)を記述する場合、通常は状態ベースのテスト(与えられた入力と目的の出力)となる動作テストを記述します。この違いは、多くの場合、TDDコミュニティではモック主義と統計主義と呼ばれています。私は両方の方法が正しいことを主張する(小さな)グループの一部です。それは、それらが異なるトレードオフを提供するだけであり、したがって、上記のような異なるシナリオに役立ちます。


1
あなたの答えはいくつかの良い点を持っていますが、私は一つのことを少し選ぶことができます。「コントローラーは通常、コンポーネントのインテグレーターなので、多くのモックが必要な統合テストを作成します」-おそらく、「コントローラーの単体テストを作成しようとすると、通常、多くのモックが必要になる」と思われたでしょう。 。「統合テスト」という用語は、モックなしのテストに適しています。モックなしで実際のコンポーネントを実際に使用して、意図したとおりに機能するかどうかを確認します。
Doc Brown

@DocBrownに感謝します。私は実際に「コンポーネント間の統合/通信をテストする単体テスト」に言及しており、実際のコンポーネントを含む統合テストの概念ではありません。
MichelHenrich 2014年

1
さて、ここで「統合テスト」という用語について同意したところで、私はあなたの答えが次の質問に直接つながると思います:「インテグレーター」の主な役割を持つコントローラーにTDD(または単体テストを書く)を使用することは本当に価値がありますか?または、これらのコンポーネントの統合テストのみを作成することをお勧めします(多分後で)。
Doc Brown

4

TDDはテストファーストの方法ですが、本番用コードを書く前にテストコードを書くのに多くの時間を費やす必要はありません。

この例では、ケントベックのTDDに関する独創的な書籍(1)で説明されているTDDの考え方は、

AccountController controller = new AccountController()

var result = controller.Index(username : "Peter", password: "whatever");

Assert.AreEqual(result.Model.Username, "Peter");

最初、あなたはあなたが仕事をするためにあなたが必要とするであろうすべてを知りません。ユーザー名を持つモデルを提供するIndexメソッドを備えたコントローラーが必要になることを知っているだけです。あなたはそれがどうやってそれをするのかまだ知りません。自分の目標を設定しました。

次に、利用可能な任意の手段を使用して、最初は正しい結果をハードコーディングするだけでそれを機能させることができます。次に、後続のリファクタリング(およびさらにテストを追加)で、一度に1ステップずつ高度な機能を追加します。TDDを使用すると、前進する必要があるのと同じくらい小さいステップを踏むことができますが、スキルと知識が許す限り、自由にステップを踏むこともできます。テストコードと本番コードの間の短いサイクルを取ることで、実行するすべての小さなステップに関するフィードバックを得て、今行ったことがうまくいったかどうか、それが以前に働いていた他のものを壊したかどうかをほぼ即座に知ることができます。

2)のロバートマーティンは、テストコードの作成と製品コードの作成の間の非常に短いサイクルタイムを提唱しています。


3

概念的に単純な単体テストでは、結局この複雑さすべてが必要になるかもしれませんが、そもそもこのようなテストを書くことはほぼ確実にありません。

最初に、最初の6行の複雑な設定は、自己完結型の再利用可能なフィクスチャコードに分解する必要があります。保守可能なプログラミングの原則は、ビジネスコードと同じようにテストコードに適用されます。2つ以上のテストに同じフィクスチャを使用する場合は、テスト内で1行しか邪魔にならないように別のメソッドにリファクタリングするか、クラスセットアップコードにリファクタリングして何もないようにする必要があります。

しかし、より重要なことは、最初にテストを作成しても、テストが永久に変更されないことを保証するものではありません。メソッド呼び出しの共同編集者がわからない場合、ほとんどの場合、最初の試行で正しいと推測することはできません。パブリックAPIが変更された場合、ビジネスコードと共にテストコードをリファクタリングしても問題はありません。そもそもTDD の目的は、正確で使いやすいAPIを作成することですが、これが100%になることはほとんどありません。常に要件事実の後で変化します。そしてこれは、ストーリーの最初のイテレーションを書いたときに存在しなかった協力者を絶対に必要とします。その場合は何もすることはありませんが、弾丸をかじって、アプリケーションに合わせて既存のテストを変更します。これらは、引用したセットアップコードの大部分がテストスイートに組み込まれる機会です。


2
最初の部分には強く反対します。テストは独立している必要があります。独立性は単体テストの保守性を向上させ、再利用の欠如は製品コードに悪影響を与えるため、単体テストではコードよりもはるかに高い要件です。
Telastyn 2014年

1
@Telastynテストは、セットアップコードを共有している間も独立できます。新しいフィクスチャを使用していることを確認する必要があります。つまり、共有セットアップメソッドを呼び出すか、暗黙的なセットアップを使用します(テストフレームワークがサポートしている場合)。
ベンジャミンホジソン

1
@BenjaminHodgson-1つのテストで共有セットアップメソッドを変更して、別のテストを中断しないようにする方法がわかりません。
Telastyn 2014年

1
@Telastynしかし、それは一般に再利用されたコードに当てはまります-クラスに複数のクライアントがあると、変更が難しくなります。すべての単体テストでフィクスチャセットアップコードのコピーと貼り付けの重複を主張していますか?
ベンジャミンホジソン

3
@Telastyn:テストを互いに独立させることがDRYの原則に違反する場合、コードのデザインを改善しようとすると必然的に問題が発生しますが、1つの再利用されたセットアップメソッドではなく、「類似したセットアップ」で30のテストメソッドを変更する必要があります。 。これは、実際にTDDに対して頻繁に聞かれる一番の議論です。リファクタリング中にテストを変更するには労力がかかり過ぎます。
Doc Brown、

2

それは、依存関係をモックしてクラスを分離するために実装する前に、テスト中のサブジェクトの内部を知る必要があるようなもので、ある種の書き込みテスト-コードの作成-必要に応じてテストを変更します。

はい、ある程度は行います。TDDの仕組みを誤解しているとは思いません。

問題は、他の人が述べたように、最初は非常に奇妙に感じられ、この方法で行うのはほとんど間違っているということです。私の意見では、実際にTDDの最大の利点であると私が感じていることを示しています。コードを記述する前に、要件を適切に理解する必要があります。

プログラマーとして、私たちはコードを書くのが好きです。それで、要件をすくい取り、できるだけ早く立ち往生するために、私たちにとって「正しい」「自然な」と感じるものは何ですか。コードベースを構築してテストすると、設計上の問題が徐々に明らかになります。したがって、リファクタリングして修正すると、物事は徐々に改善され、目標に向かって進みます。

これは楽しいですが、特に効率的な方法ではありません。ソフトウェアモジュールが最初に何をすべきかを適切に把握し、テストを停止してからコードを記述する方がはるかに優れています。リファクタリングが少なく、テストメンテナンスが少なくて済むので、ブロック外の優れたアーキテクチャを実現できます。

私はTDDをあまり行いませんが、「100%コードカバレッジ」のマントラは無意味だと思います。特にあなたのような場合に。しかし、TDDを採用することには、コード全体で設計と保守が適切に行われていることを確認する上で非常に役立つため、依然として大きな価値があります。

要約すると、この奇妙なものを見つけているという事実は、おそらくあなたが正しい道を進んでいることの良い兆候です。


0

データのモックは、ダミーデータを使用する練習にすぎません。

ARRANGE | ACT | 主張する

TDDは通常、テストを作成し、それらのテストを「合格」することを検証することです。最初は、そのテストを検証するコードがまだ作成されていないため、最初のテストは失敗します。これは実際には特定の種類のテストだと思います。「赤/緑」のテスト。これは、今日の「テスト駆動」メソッドのソースです。

一般に、テストは、より大きな画像コードを機能させるロジックの小さなナゲットを検証します。関数の最小レベルから始めて、より複雑な関数まで進んでいきます。

はい、セットアップまたは「モッキング」が多少強烈になる場合があります。そのため、moqフレームワークを使用することをお勧めします。ただし、コアビジネスロジックに焦点を当てている場合、テストは、それが機能するという有益な保証になります。期待どおり、意図したとおり。

個人的には、コントローラーが使用しているすべてのものが動作するようにテストされているため、私はコントローラーをテストしていません。通常、フレームワークをテストする必要はありません。

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