4

NuGetGalleryでユニットテストが行​​われる方法を見てきました。コントローラをテストすると、サービスクラスがモックされていることがわかりました。これは私にとって理にかなっています。なぜなら、コントローラーロジックをテストしている間、下のアーキテクチャーレイヤーについて心配したくなかったからです。このアプローチをしばらく使用した後、サービスクラスが変更されたときに、コントローラーテスト全体でモックの修正を頻繁に実行していることに気付きました。この問題を解決するために、私より賢い人に相談することなく、私はこのようなテストを書き始めました(心配しないでください、私はそれほど遠くまで来ていません):

public class PersonController : Controller
{
    private readonly LESRepository _repository;

    public PersonController(LESRepository repository)
    {
        _repository = repository;
    }

    public ActionResult Index(int id)
    {
        var model = _repository.GetAll<Person>()
            .FirstOrDefault(x => x.Id == id);

        var viewModel = new VMPerson(model);
        return View(viewModel);
    }
}

public class PersonControllerTests
{
    public void can_get_person()
    {
        var person = _helper.CreatePerson(username: "John");
        var controller = new PersonController(_repository);
        controller.FakeOutContext();

        var result = (ViewResult)controller.Index(person.Id);
        var model = (VMPerson)result.Model;
        Assert.IsTrue(model.Person.Username == "John");
    }
}

私は実際のデータベースを使用しているので、これは統合テストになると思います(インメモリデータベースが望ましいです)。データベースにデータを配置することからテストを開始します(各テストはトランザクションで実行され、テストが完了するとロールバックされます)。次に、コントローラーを呼び出します。ビューに送信されるモデルに、データベースに入れたレコード(アサーション)が必要なだけで、データベースから(リポジトリまたはサービスクラスを介して)データを取得する方法はまったく気にしません。 。このアプローチの優れた点は、コントローラーのテストを変更せずに、多くの場合、複雑さのレイヤーを追加し続けることができることです。

public class PersonController : Controller
{
    private readonly LESRepository _repository;
    private readonly PersonService _personService;

    public PersonController(LESRepository repository)
    {
        _repository = repository;
        _personService = new PersonService(_repository);
    }

    public ActionResult Index(int id)
    {
        var model = _personService.GetActivePerson(id);
        if(model  == null)
          return PersonNotFoundResult();

        var viewModel = new VMPerson(model);
        return View(viewModel);
    }
}

これで、PersonServiceのインターフェイスを作成して、コントローラーのコンストラクターに渡していないことに気付きました。その理由は、1)PersonServiceをモックする予定がないこと、および2)PersonControllerが1つのタイプのPersonServiceにのみ依存する必要があるため、依存関係を注入する必要がないと感じたことです。

私はユニットテストに不慣れで、自分が間違っていることが示されることを常に嬉しく思っています。コントローラーをテストする方法が本当に悪い考えである理由を指摘してください(テストの実行にかかる時間が明らかに増加することを除けば)。

4

4 に答える 4

5

うーん。ここにいくつかのことがあります。

まず、コントローラーメソッドをテストしようとしているようです。素晴らしい :)

つまり、これは、コントローラーが必要とするものはすべてモックする必要があることを意味します。それの訳は

  1. その依存関係の中で何が起こるかについて心配する必要はありません。
  2. 依存関係が呼び出され/実行されたことを確認できます。

では、あなたが何をしたかを見てみましょう。もう少しテストしやすくするためにリファクタリングできるかどうかを確認します。

-覚えておいてください-私はコントローラーメソッドをテストしていますが、コントローラーメソッドが呼び出す/依存するものではありません。

つまり、これは、サービスインスタンスまたはリポジトリインスタンス(どちらのアーキテクチャの方法に従うか)を気にしないことを意味します。

注:私は物事をシンプルにしたので、たくさんのがらくたなどを取り除きました。

インターフェース

まず、リポジトリのインターフェースが必要です。これは、メモリ内リポジトリ、エンティティフレームワークリポジトリなどとして実装できます。理由はすぐにわかります。

public interface ILESRepository
{
    IQueryable<Person> GetAll();
}

コントローラ

ここでは、インターフェースを使用します。これは、モックIRepositoryまたは実際のインスタンスを使用するのが本当に簡単で素晴らしいことを意味します。

public class PersonController : Controller
{
    private readonly ILESRepository _repository;

    public PersonController(ILESRepository repository)
    {
       if (repository == null)
       {
           throw new ArgumentNullException("repository");
       }
        _repository = repository;
    }

    public ActionResult Index(int id)
    {
        var model = _repository.GetAll<Person>()
            .FirstOrDefault(x => x.Id == id);

        var viewModel = new VMPerson(model);
        return View(viewModel);
    }
}

単体テスト

わかりました-これが魔法のマネーショットです。まず、偽の人物を作成します。ここで私と一緒に働いてください...これをダニのどこで使用するかをお見せします。それはあなたPOCOのの退屈で単純なリストです。

public static class FakePeople()
{
    public static IList<Person> GetSomeFakePeople()
    {
        return new List<Person>
        {
            new Person { Id = 1, Name = "John" },
            new Person { Id = 2, Name = "Fred" },
            new Person { Id = 3, Name = "Sally" },
        }
    }
}

これで、テスト自体ができました。xUnitテストフレームワークとモックに使用していますmoq。ここでは、どのフレームワークでも問題ありません。

public class PersonControllerTests
{
    [Fact]
    public void GivenAListOfPeople_Index_Returns1Person()
    {
        // Arrange.
        var mockRepository = new Mock<ILESRepository>();
        mockRepository.Setup(x => x.GetAll<Person>())
                                   .Returns(
                                FakePeople.GetSomeFakePeople()
                                          .AsQueryable);
        var controller = new PersonController(mockRepository);
        controller.FakeOutContext();

        // Act.
        var result = controller.Index(person.Id) as ViewResult;

        // Assert.
        Assert.NotNull(result);
        var model = result.Model as VMPerson;
        Assert.NotNull(model);
        Assert.Equal(1, model.Person.Id);
        Assert.Equal("John", model.Person.Username);

        // Make sure we actually called the GetAll<Person>() method on our mock.
        mockRepository.Verify(x => x.GetAll<Person>(), Times.Once());
    }
}

さて、私がしたことを見てみましょう。

まず、がらくたを整理します。まず、のモックを作成しますILESRepository。それから私は言います:誰かがGetAll<Person>()メソッドを呼び出す場合、まあ..データベースやファイルなどを実際にヒットしないでください..で作成された人のリストを返すだけですFakePeople.GetSomeFakePeople()

だからこれはコントローラーで起こることです...

var model = _repository.GetAll<Person>()
                       .FirstOrDefault(x => x.Id == id);

まず、モックにGetAll<Person>()メソッドをヒットするように依頼します。人々のリストを返すように「設定」しただけです..それで、3つのPersonオブジェクトのリストができました。次に、FirstOrDefault(...)この3つのオブジェクトのリストでaを呼び出しPersonます。これは、の値に応じて、単一のオブジェクトまたはnullを返しますid

多田!それがマネーショットです:)

次に、単体テストの残りの部分に戻ります。

私たちActそしてそれから私たちAssert。難しいことは何もありません。ボーナスポイントについては、私verifyたちが実際にメソッドと呼んでいるのは、コントローラーのメソッドGetAll<Person>()内のモックです。Indexこれは、コントローラーロジック(テスト中)が正しく行われたことを確認するための安全呼び出しです。

場合によっては、人が悪いデータを渡したなど、悪いシナリオをチェックしたいことがあります。これは、モックメソッド(正しい)にたどり着くことがない可能性があることを意味しますverify。そのため、モックメソッドが呼び出されることはありません。

わかりました-質問、クラス?

于 2012-04-28T02:52:49.467 に答える
2

インターフェイスをモックする予定がない場合でも、コンストラクター内にオブジェクトを作成してオブジェクトの実際の依存関係を隠さないことを強くお勧めします。単一責任の原則に違反し、テスト不可能なコードを記述しています。

テストを書くときに考慮すべき最も重要なことは、「テストを書くための魔法の鍵はない」ということです。テストの作成に役立つツールはたくさんありますが、既存のコードをハッキングしてテストを作成するのではなく、テスト可能なコードを作成することに真剣に取り組む必要があります。テストは通常​​、単体テストではなく統合テストになります。 。

コンストラクター内に新しいオブジェクトを作成することは、コードがテスト可能でないことを示す最初の大きなシグナルの1つです。

これらのリンクは、私がテストを書き始めるための移行を行っていたときに私を大いに助けました、そしてあなたが始めた後、それはあなたの毎日の仕事の自然な部分になり、あなたは私が自分自身を想像できないテストを書くことの利点を気に入るはずですもうテストなしでコードを書く

クリーンなコードガイド(Googleで使用):http://misko.hevery.com/code-reviewers-guide/

詳細については、以下をお読みください。

http://misko.hevery.com/2008/09/30/to-new-or-not-to-new/

MiskoHeveryからキャストされたこのビデオをご覧ください

http://www.youtube.com/watch?v=wEhu57pih5w&feature=player_embedded

編集:

Martin Fowlerによるこの記事では、ClassicalとMockistのTDDアプローチの違いについて説明しています。

http://martinfowler.com/articles/mocksArentStubs.html

要約すると:

  • 従来のTDDアプローチ:これは、Webサービスやデータベースなどの外部サービスを除いて、代替またはダブル(モック、スタブ、ダミー)を作成せずに可能なすべてをテストすることを意味します。クラシックテスターは、外部サービスにのみdoubleを使用します

    • 利点:テストするときは、実際にアプリケーションの配線ロジックとロジック自体をテストしています(単独ではありません)。
    • 短所:エラーが発生した場合、何百ものテストが失敗する可能性があり、責任のあるコードを見つけるのが困難になります
  • Mockist TDDアプローチ:Mockistアプローチに従う人々は、すべての依存関係に対してdoubleを作成するため、すべてのコードを分離してテストします。

    • 利点:アプリケーションの各部分を個別にテストしています。エラーが発生した場合、いくつかのテストが失敗するため、エラーが発生した場所が正確にわかります。理想的には1つだけです。
    • 短所:すべての依存関係を2倍にする必要があるため、テストが少し難しくなりますが、AutoFixtureなどのツールを使用して、依存関係の2倍を自動的に作成できます。

これは、テスト可能なコードの記述に関するもう1つの優れた記事です。

http://www.loosecouples.com/2011/01/how-to-write-testable-code-overview.html

于 2012-04-27T23:55:29.933 に答える
1

いくつかの欠点があります。

まず、外部コンポーネント(ライブデータベースなど)に依存するテストがある場合、そのテストは実際には予測できなくなります。ネットワークの停止、データベースアカウントのパスワードの変更、一部のDLLの欠落など、さまざまな理由で失敗する可能性があります。したがって、テストが突然失敗した場合、欠陥どこにあるかをすぐに確認することはできません。データベースの問題ですか?あなたのクラスのいくつかのトリッキーなバグ?

どのテストが失敗したかを知るだけですぐにその質問に答えることができれば、羨ましいほどの品質の欠陥ローカリゼーションが得られます。

次に、データベースに問題がある場合、それに依存するすべてのテストが一度に失敗します。おそらく原因がわかるので、これはそれほど深刻ではないかもしれませんが、それぞれを調べるのが遅くなることを保証します。50のテストのそれぞれで例外を見たくないので、広範囲にわたる失敗は実際の問題を隠すことができます。

実行時間以外の要素についても聞きたいと思いますが、それは本当に重要です。テストをできるだけ頻繁に実行する必要がありますが、実行時間が長くなるとそれができなくなります。

私には2つのプロジェクトがあります。1つは10秒で実行される600以上のテストで、もう1つは50秒で実行される40以上のテストです(このプロジェクトは実際には意図的にデータベースと通信します)。開発中は、より高速なテストスイートをより頻繁に実行ます。どちらが使いやすいと思いますか?

とはいえ、外部コンポーネントのテストには価値があります。ユニットテストをしているときだけではありません。統合テストはより脆弱で、時間がかかります。それはそれらをより高価にします。

于 2012-04-27T23:58:01.803 に答える
1

単体テストでデータベースにアクセスすると、次のような結果になります。

  1. パフォーマンス。データベースへのデータの入力とアクセスには時間がかかります。テストが多ければ多いほど、待ち時間は長くなります。モックを使用した場合、データベースを直接使用した場合の数秒と比較して、コントローラーのテストはそれぞれ数ミリ秒で実行される可能性があります。
  2. 複雑さ。共有データベースの場合、複数のエージェントが同じデータベースに対してテストを実行している同時実行の問題に対処する必要があります。データベースをプロビジョニングしたり、構造を作成したり、データを入力したりする必要があります。かなり複雑になります。
  3. カバレッジ。いくつかの条件は、あざけることなしにテストすることはほとんど不可能であることに気付くでしょう。例としては、データベースがタイムアウトしたときに何をすべきかを確認することが含まれる場合があります。または、電子メールの送信が失敗した場合の対処方法。
  4. メンテナンス。データベーススキーマへの変更は、特に頻繁に行われる場合、データベースを使用するほとんどすべてのテストに影響を与えます。最初は10回のテストがあるとそれほど多くないように見えるかもしれませんが、2000回のテストがあるときを考えてみてください。また、ビジネスルールを検証するためにデータベースに入力されたデータを変更する必要があるため、ビジネスルールの変更とテストの適応がより複雑になる場合があります。

ビジネスルールをテストする価値があるかどうかを尋ねる必要があります。ほとんどの場合、答えは「いいえ」です。

私が従うアプローチは次のとおりです。

  1. 依存関係をモックアウトし、発生する可能性のある条件(データベースエラーなど)をシミュレートすることによるユニットクラス(コントローラー、サービスレイヤーなど)。これらのテストはビジネスロジックを検証し、決定パスを可能な限りカバーすることを目的としています。PEXのようなツールを使用して、思いもよらなかった問題を強調します。PEXが強調する問題のいくつかを修正した後、アプリケーションがどれほど堅牢(かつ回復力)になるかに驚かれることでしょう。
  2. データベーステストを作成して、使用しているORMが基盤となるデータベースで機能することを確認します。EFや他のORMが特定のデータベースエンジン(およびバージョン)で抱えている問題に驚かれることでしょう。これらのテストは、パフォーマンスを調整し、データベースとの間で送受信されるクエリとデータの量を減らすためにも役立ちます。
  3. ブラウザを自動化し、システムが実際に機能することを確認するコード化されたUIテストを記述します。この場合、事前にデータベースにデータを入力します。これらのテストは、私が手動で行ったであろうテストを単純に自動化します。目的は、重要な部分がまだ機能していることを確認することです。
于 2012-04-27T23:59:48.450 に答える