4

私は単体テストと TDD に少し慣れていないので、この投稿は議論のきっかけとなることを目的としています。

私は現在、いくつかのデータベースとやり取りする .NET プロセスの単体テストをいくつか作成しており、テスト内のさまざまなエッジ ケースをカバーするためにモック データベース コンテキストを使用し、プログラム自体の例外処理を検証しています。そうは言っても、私の単体テストの中には有効なデータを使用するものもあれば、そうでないものもあります。

有効な/偽のデータをモック データベース コンテキストに追加する際の推奨されるベスト プラクティスに関するフィードバックを探しています。人々がこれをさまざまな方法で行うのを見てきました (例: リポジトリ パターンを実装する、モック データを .csv ファイルに追加してプロジェクトの一部にするなど...)。

現在、ターゲット DBSurveyのテーブルにオブジェクトを追加するためにリポジトリ パターンを使用することを考えています。Surveys

まず、私はインターフェースを持っています:

public interface ISurveyRepository
{
   IQueryable<Survey> SurveySeries { get; }
}

これは、単体テストで必要に応じて、モック/有効なデータ リポジトリの両方に実装されます。

class FakeSurveyRepository : ISurveyRepository
{
   private static IQueryable<Survey> fakeSurveySeries = new List<Survey> {
      new Survey { id = 1, SurveyName="NotValid1", SurveyData="<data>fake</data>"},
      new Survey { id = 2, SurveyName="NotValid2", SurveyData="<data>super fake</data>"},
      .........,
      new Survey {id = 10, SurveyName="NotValid10", SurveyData="<data>the fakest</data>" }       
   }.AsQueryable();

   public IQueryable<Survey> SurveySeries 
   { 
      get { return fakeSurveySeries; }
   }
}
// RealSurveyRepository : ISurveyRepository is similar to this, but with "good" data

次に、コンストラクターでシリーズへの参照を渡すことにより、このデータを偽/有効なデータとして消費するクラスを作成します。

public class SurveySeriesProcessor
{
   private ISurveyRepository surveyRepository;

   public SurveySeriesProcessor( ISurveyRepository surveyRepository )
   {
       this.surveyRepository = surveyRepository;
   }

   public IQueryable<Survey> GetSurveys()
   {
      return surveyRepository.SurveySeries
   }
} 

そして、次のようなテストでこれらのオブジェクトを使用してアプローチできます。

[TestClass]
public class SurveyTests
{
    [TestMethod]
    WhenInvalidSurveysFound_SurveyCopierThrowsInvalidSurveyDataErrorForEach()
    {
       // create mocking DB context and add fake data
       var contextFactory = new ContextFactory( ContextType.Mocking );
       var surveySeriesProcessor = new SurveySeriesProcessor( new FakeSurveyRepository() );

       foreach(Survey surveyRecord in surveySeriesProcessor.GetSurveys() )
       {
          contextFactory.TargetDBContext.Surveys.AddObject( surveyRecord );
       }
       // instantiate object being tested and run it against fake test data
       var testSurveyCopier = new SurveyCopier( contextFactory );
       testSurveyCopier.Start();
       // test behavior
       List<ErrorMessage> errors = testSurveyCopier.ErrorMessages;
       errors.Count.ShouldEqual( surveySeriesProcessor.GetSurveys().Count );
       foreach(ErrorMessage errMsg in errors)
       {
          errMsg.ErrorCode.ShouldEqual(ErrorMessage.ErrorMessageCode.InvalidSurveyData);
       }
    }
}

ISurveyRepository注: 提供されているサンプル コードでは、実装するクラスがシリーズを として返す必要は必ずしもないことを認識していますIQueryable<Survey>(それらは である可能性が非常に高いですList<Survey>)。ただし、将来的にはインターフェイスとこれらのクラスの機能を拡張して、LINQ クエリに追加された特定の基準に基づいて偽/有効なシリーズを除外する予定ですIQueryable<>。これは、私が考えていることの基本原則を伝えるために設計されたモックアップ コードです。

これらすべてを念頭に置いて、私が求めているのは次のとおりです。

  1. そのようなシナリオで私が取ることができる代替アプローチに関して何か提案はありますか?
  2. 過去にどのような方法を採用してきましたか。その中で気に入った/気に入らなかった点は何ですか? 最も維持しやすいのはどれでしたか?
  3. 私が投稿した内容を踏まえて、単体テストに対する私の一般的なアプローチに欠陥があることに気づきましたか? 簡潔でエレガントで的を射たものではなく、あまりにも多くの領域をカバーしようとする単体テストを書いているように感じることがあります。

これは、ある程度オープンな議論になることを意図しています。これは私が今までに書いた最初の単体テストのセットであることを覚えておいてください (ただし、この件に関するかなりの量の文献を読みました)。

4

1 に答える 1

8

順調に進んでいると思います。

個人的には、同じ状況で、リポジトリスタイルのパターンを扱っていた場合、

public interface IRepository<T>
{
    IEnumerable<T> GetAll();
}


public class PonyRepository : IRepository<Pony>
{
    IEnumerable<Pony> GetAll();
}

実際に必要なデータを提供するために、私は通常、必要なデータをオンデマンドで提供するTestObjectsまたはTestFakesクラスを作成します。

public class FakeStuff
{
     public static IEnumerable<Pony> JustSomeGenericPonies(int numberOfPonies)
     {
        // return just some basic list
         return new List<Pony>{new Pony{Colour = "Brown", Awesomeness = AwesomenessLevel.Max}};

         // or could equally just go bananas in here and do stuff like...
         var lOfP = new List<Pony>();
         for(int i = 0; i < numberOfPonies; i++)
         {
             var p = new Pony();
             if(i % 2 == 0) 
             {
                 p.Colour = "Gray";
             }
             else
             {
                 p.Colour = "Orange"; 
             }

             lOfP.Add(p);
         }

         return lOfP;
     }
}

そして、これをそのままテストします。

[Test]
public void Hello_I_Want_to_test_ponies()
{
    Mock<IRepository<Pony> _mockPonyRepo = new Mock<IRepository<Pony>>();
    _mockPonyRepo.SetUp(m => m.GetAll()).Returns(FakeStuff.JustSomeGenericPonies(50));

    // Do things that test using the repository
}

したがって、これにより、偽のデータをリポジトリから除外し、独自の場所に配置することで、偽のデータの再利用性が実現します。つまり、リポジトリが関係する場所だけでなく、テストでポニーのリストが必要な場所であればどこでも、このポニーのリストを呼び出すことができます。

特定のテストケースに特定のデータが必要な場合は、あなたが持っていたようなものを実装しますが、その特定の偽のリポジトリが何のためにあるのかについてもう少し明確にします。

public class FakePonyRepositoryThatOnlyReturnsBrownPonies : IRepository<Pony>
{
    private List<Pony> _verySpecificAndNotReusableListOfOnlyBrownPonies = new List....

    public IEnumerable<Pony> GetAll()
    {
        return _verySpecificAndNotReusableListOfOnlyBrownPonies;
    }
}

public class FakePonyRepositoryThatThrowsExceptionFromGetAll : IRepository<Pony>
{
    public IEnumerable<Pony> GetAll()
    {
        throw new OmgNoPoniesException();
    }
}

CSVファイルについても言及されました-これは実行可能である可能性があります(過去にXMLを使用したことがあります)が、CSVまたはXMLで偽のデータを保持することは、SQLCEまたはいくつかの同等物。ただし、どちらも保守性が低く、単体テストの観点から、メモリ内の偽のオブジェクトを使用するよりも低速です。シリアル化やIOなどを具体的にテストしない限り、個人的にはファイルベースのアプローチを使用しなくなりました。

そのすべての中に何か役に立つものがあることを願っています...

于 2012-11-15T20:29:23.090 に答える