.NETでのデータベーステスト







データベースアプリケーションをテストする一般的な.NETアプローチは、依存性注入です。 後でテストで置き換えることができる抽象化を作成することにより、ベースで動作するコードをメインロジックから分離することが提案されています。 これは非常に強力で柔軟なアプローチですが、それにも関わらずいくつかの欠点があります-複雑さの増加、ロジックの分離、型の数の爆発的な増加。 前の記事で詳しく説明します。.NET(Javaなど)またはWiki / Dependency Injection でのテストに問題があります







動的言語の世界で広く普及しているより単純なアプローチがあります。 このアプローチでは、テストで制御できる抽象化を作成する代わりに、ベース自体を制御することを提案します。 テストフレームワークは、各テストのクリーンベースを提供し、テストスクリプトを作成できます。 それはより簡単で、テストにより多くの自信を与えます。











前の記事で示したように、この例は非常に重要です。 それが失敗した場合、例ではなくアプローチ自体が批判されます。 ここで私は彼にもっと注意を払ったが、もちろん彼は完璧でもない:



商品の在庫会計には特定のアプリケーションがあります。 転送文書を使用して、商品を倉庫間で移動できます。 指定した時点で指定した倉庫の残高を取得できる方法が必要です。



これを行うには、次のメソッドを導入します(テストする必要があります)。



public class ReminesService { RemineItem[] GetReminesFor(Storage storage, DateTime time) { ... } }
      
      





この記事ではこのメソッドを実装しませんが、githubのリポジトリにあります。



テストデータベース



テストにはデータベースが必要です。 単純なプロジェクトの場合、SQLiteを使用できます。これは、テストの速度とその信頼性の間の良い妥協点です。 より複雑な場合は、開発時と同じデータベースを使用することをお勧めします。 ほとんどの場合、これは問題ではありません。MySqlとPostgreSqlは軽量で、SQLServerにはLocalDbモードがあります。



SQLServerを使用する場合は、テストデータベースにLocalDbモードを使用すると便利です。完全に機能する一方で、完全なデータベースよりもはるかに簡単で高速です。 これを行うには、テストプロジェクトでApp.configを構成します。



SQLServer LocalDbの構成
 <?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="MSSQLLocalDB" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration>
      
      







枠組み



このアプローチは.NETでは非常にまれなので、その実装用の既製のライブラリはほとんどありません。 したがって、この領域の開発を小さなDbTestライブラリに入れました。 githubでソースコードと例を見るか、nugetを使用してプロジェクトにインストールできます。 プロジェクトは暫定版であり、APIが変更される可能性があるため、注意してください。



初期データ



実際のシステムでは、モデル間に多くの関係がありますが、少なくとも1つの行をターゲットテーブルに挿入するには、多くの関連テーブルに入力する必要があります。 たとえば、製品(Good)は製造業者(Manufacturer)を指し、製造業者(Manufacturer)は国(Country)を指します。



テストシナリオのさらなる作成を簡素化するには、システムに共通する最小限のデータセットを作成する必要があります。



それをもう少し楽しくするために、ウイスキーのボトルを商品として取りましょう。 依存関係のないモデルから始めましょう-原産国(国):



 public class Countries : IModelFixture<Country> { public string TableName => "Countries"; public static Country Scotland => new Country { Id = 1, Name = "Scotland", IsDeleted = false }; public static Country USA => new Country { Id = 2, Name = "USA", IsDeleted = false }; }
      
      





フレームワークがこれが初期データの記述であることを理解するために、クラスはIModelFixture<T>



インターフェイスを実装する必要があります。 モデルインスタンスは、他のフィクスチャやテストからのアクセスを許可するために静的に宣言されます。 主キー( Id



)を明示的に指定し、同じモデル内での一意性を追跡する必要があります。



メーカーを作成できるようになりました:



 class Manufacturers : IModelFixture<Manufacturer> { public string TableName => "Manufacturers"; public static Manufacturer BrownForman => new Manufacturer { Id = 1, Name = "Brown-Forman", CountryId = Countries.USA.Id, IsDeleted = false }; public static Manufacturer TheEdringtonGroup => new Manufacturer { Id = 2, Name = "The Edrington Group", CountryId = Countries.Scotland.Id, IsDeleted = false }; }
      
      





そして商品:



 public class Goods : IModelFixture<Good> { public string TableName => "Goods"; public static Good JackDaniels => new Good { Id = 1, Name = "Jack Daniels, 0.5l", ManufacturerId = Manufacturers.BrownForman.Id, IsDeleted = false }; public static Good FamousGrouseFinest => new Good { Id = 2, Name = "The Famous Grouse Finest, 0.5l", ManufacturerId = Manufacturers.TheEdringtonGroup.Id, IsDeleted = false }; }
      
      





外部キーに注意してください-それらは明示的に指定されていませんが、別のフィクスチャを参照しています。



このアプローチには、sqlファイルまたはフィクスチャjsonファイルよりも多くの利点があります。





重要! このアプローチには欠点があります。静的プロパティにアクセスするたびに、モデルのインスタンスとそれに依存するすべてのモデル(およびその依存関係も)が作成されます。 パフォーマンスの問題または循環参照が発生する場合は、遅延初期化Lazy <T>でこれを修正できます。



 private static Good _famousGrouseFinest = new Lazy<Good>(() => new Good { Id = 2, Name = "The Famous Grouse Finest, 0.5l", ManufacturerId = Manufacturers.TheEdringtonGroup.Id, IsDeleted = false }; public static Good FamousGrouseFinest => _famousGrouseFinest.Value;
      
      





環境の準備



テスト環境は主にデータベースですが、シングルトン変数や静的変数にすることもできます(たとえば、 HttpContext



はasp.netで設定できます)。 これらのすべての操作を1か所に集めて、各テストの前に実行することをお勧めします。 そのような場所を「世界」と名付けました。 データベースを準備するには、 ResetWithFixtures



メソッドを呼び出して、初期フィクスチャのリストをそこに渡す必要があります。



 static class World { public static void InitDatabase() { using (var context = new MyContext()) { var dbTest = new EFTestDatabase<MyContext>(context); dbTest.ResetWithFixtures( new Countries(), new Manufacturers(), new Goods() ); } } public static void InitContextWithUser() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://your-domain.com", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new GenericPrincipal( new GenericIdentity("root"), new string[0] ); } }
      
      





静的変数とシングルトーンを設定する機能は、アーキテクチャを変更するのがそれほど簡単ではないレガシーコードをテストする場合に特に重要ですが、テストが緊急に必要です。 環境設定をいくつかの方法に分割すると、各テスト用に個別の環境を準備できます。 たとえば、単体テストでは、ベースは使用されず、ベースのクリアは意味がありません。 または、システムの状態(許可ユーザーと無許可ユーザー)ごとに異なる環境を準備する必要がある場合があります。



テストケース作成



テストでは多くの準備作業を行う必要があります。テストのアレンジフェーズは最も責任があり、困難です。 したがって、このプロセスを簡素化し、コードを読みやすくするヘルパーを作成することが望ましいです。 便利なメカニズムの1つは、ModelBuilderの作成です。ModelBuilderは、エンティティを作成し、それらをデータベースに格納し、将来の使用のためにインスタンスを返します。



 public class ModelBuilder { public MoveDocument CreateDocument(string time, Storage source, Storage dest) { var document = new MoveDocument { Number = "#", SourceStorageId = source.Id, DestStorageId = dest.Id, Time = ParseTime(time), IsDeleted = false }; using (var db = new MyContext()) { db.MoveDocuments.Add(document); db.SaveChanges(); } return document; } public MoveDocumentItem AddGood(MoveDocument document, Good good, decimal count) { var item = new MoveDocumentItem { MoveDocumentId = document.Id, GoodId = good.Id, Count = count }; using (var db = new MyContext()) { db.MoveDocumentItems.Add(item); db.SaveChanges(); } return item; } }
      
      





テスト中



すべてをまとめて、何が起こったのかを見てみましょう。



 [SetUp] public void SetUp() { World.InitDatabase(); //      } [Test] public void CalculateRemainsForMoveDocuments() { /// ARRANGE -    var builder = new ModelBuilder(); //      var doc1 = builder.CreateDocument("15.01.2016 10:00:00", Storages.MainStorage, Storages.RemoteStorage); builder.AddGood(doc1, Goods.JackDaniels, 10); builder.AddGood(doc1, Goods.FamousGrouseFinest, 15); //      var doc2 = builder.CreateDocument("16.01.2016 20:00:00", Storages.RemoteStorage, Storages.MainStorage); builder.AddGood(doc2, Goods.FamousGrouseFinest, 7); /// ACT -    var remains = RemainsService.GetRemainFor(Storages.RemoteStorage, new DateTime(2016, 02, 01)); /// ASSERT -   Assert.AreEqual(2, remains.Count); Assert.AreEqual(10, remains.Single(x => x.GoodId == Goods.JackDaniels.Id).Count); Assert.AreEqual(8, remains.Single(x => x.GoodId == Goods.FamousGrouseFinest.Id).Count); }
      
      





テストコードでの初期フィクスチャの使用に注意してください。

Storages.MainStorage



Goods.JackDaniels



Goods.FamousGrouseFinest



など



すでにデータベースにあるすべてのオブジェクトが手元にあり、テストの任意のフェーズで使用できることが非常に便利です。



まとめ



このアプローチは、厳密に型指定された言語の世界では不当にバイパスされており、動的言語では非常に普及しています。 これは特効薬ではなく、DIに代わるものではありませんが、多くの場合、非常に便利で適切なアプローチです。



DIと比較して、このデータベースを使用したテストには次の利点があります。





統合テストでの軟膏の最大のフライはランタイムであり、はるかに遅いですが、これは解決可能な問題です。 少なくともサーバー時間は、開発者の時間よりもはるかに安価です。



DIは私にとって非常に優れたお気に入りのテクニックであり、自尊心のあるプログラマーを使用できるはずです。 ただし、テストの分野では、長所と短所が異なる非常に優れた代替手段があります。 私は、武器庫に多くの方法とアプローチを持ち、それぞれが状況に応じて適用されることを望んでいます。



便利なリンク



DbTest (テストフレームワークと記事のサンプルを含むリポジトリ)

スモック (静的システムメソッドのモック)



All Articles