ユニット模擬試験

最近、テストによる開発をテーマにした多くの出版物が登場し続けています。 このトピックは十分に興味深いものであり、時間の一部を研究に費やす価値があります。 私たちのチームでは、1年以上にわたって単体テストを使用しています。 この記事では、何が起こったのか、最終的にどのような経験を得たかについてお話したいと思います。



例はC#言語と.NETプラットフォームに関連して与えられていることをすぐに言わなければなりません。 したがって、他の言語/プラットフォームでは、アプローチと実装が異なる場合があります。



だから...



単体テストとは何ですか?



単体テストはソフトウェア製品の機能と一致する必要があるという事実に加えて、主な要件は速度です。 テストスイートを起動した後、開発者が休憩を取ることができる場合(私の場合、スモークブレークの場合)、そのような起動はますます少なくなります(再び、私の場合、ニコチンの過剰摂取の恐れのため)。 その結果、単体テストがまったく実行されず、結果として、それらを記述する意味が失われることが判明する場合があります。 プログラマは、いつでもテストスイート全体を実行できる必要があります。 そして、このセットはできるだけ速く実行する必要があります。



単体テストの速度を確保するために、どのような要件を遵守する必要がありますか?



テストは小さくする必要があります



理想的なケースでは、テストごとに1つアサートします。 単体テストでカバーされる機能が小さいほど、テストの実行は速くなります。



ところで、デザインの話題について。 「アレンジアクトアサート」として定式化されたアプローチが本当に気に入っています。

その本質は、単体テスト(テストデータの初期化、

プリセット)、アクション(実際にテストされているもの)および事後条件(これは

アクションの結果)。 この設計により、テストが読みやすくなり、テストが簡単になります

テストされた機能のドキュメントとして使用します。



開発でJetBrainsのReSharperを使用する場合、テストケースのブランクを作成するために使用されるテンプレートを構成することは非常に便利です。 たとえば、テンプレートは次のようになります。



[Test] public void Test_$METHOD_NAME$() { //arrange $END$ //act //assert Assert.Fail("Not implemented"); }
      
      







そして、この方法で書かれたテストは次のようになります(すべての名前は架空のものであり、偶然の一致はランダムです)。



 [Test] public void Test_ForbiddenForPackageChunkWhenPackageNotFound() { //arrange var packagesRepositoryMock = _mocks.Create<IPackagesRepository>(); packagesRepositoryMock .Setup(r => r.FindPackageAsync(_packageId)) .Returns(Task<DatabasePackage>.Factory.StartNew(() => null)); Register(packagesRepositoryMock.Object); //act var message = PostChunkToServer(new byte[] { 1, 2, 3 }); //assert _mocks.VerifyAll(); Assert.That(message.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); }
      
      







テストは環境(データベース、ネットワーク、ファイルシステム)から分離する必要があります



このアイテムはおそらく最も物議を醸すものです。 多くの場合、文献では、電卓や電話帳の開発などのタスクでTDDを使用する例が検討されています。 これらのタスクは現実と離婚しており、開発者は自分のプロジェクトに戻って、知識とスキルを日常業務に適用する方法を知らないことは明らかです。 悪名高い計算機に似た最も単純なケースは単体テストでカバーされ、残りの機能は同じレール上で開発されます。 結果として、単純なコードをとにかくデバッグできれば、単体テストに時間を費やす理由の理解は失われ、複雑なコードはとにかくテストでカバーされません。



実際、単体テストでデータベースまたはファイルシステムを使用してもかまいません。 したがって、少なくとも、システムのコアを構成する機能の機能性を確認できます。 唯一の質問は、テストの分離とテストの実行速度のバランスを維持しながら、最も効率的な方法で外部環境を使用する方法ですか?



ケース1.データアクセス層(MS SQL Server)



プロジェクトの開発でMS SQLサーバーを使用する場合、この質問に対する答えは、インストール済みのMS SQLサーバーのインスタンス(Express、Enterprise、またはDeveloper Edition)を使用してテストデータベースを展開することです。 同様のデータベースは、MS SQL Management Studioで使用される標準メカニズムを使用して作成し、単体テストを使用してプロジェクトに配置できます。 このようなデータベースを使用する一般的なアプローチは、テストを実行する前にテストデータベースを展開し(たとえば、NUnitを使用するときにSetUp属性でマークされたメソッドで)、データベースにテストデータを入力し、これらの既知のテストデータのリポジトリまたはゲートウェイの機能を確認することです。 さらに、RAMディスクを作成および管理するアプリケーションを使用して、テストデータベースをハードディスクとメモリの両方に展開できます。 私が現在取り組んでいるプロジェクトは、 SoftPerfect RAM Diskアプリケーションを使用しているとしましょう。 単体テストでRAMディスクを使用すると、テストデータベースがハードディスクに展開されたときに発生するI / O操作中に発生する遅延を減らすことができます。 もちろん、サードパーティのソフトウェアを開発者の環境に導入する必要があるため、このアプローチは理想的ではありません。 一方、開発環境が原則として1回(十分に、またはまれに)展開されることを考えると、この要件はそれほど面倒ではないようです。 また、システムの最も重要な層の1つの正しい動作を制御することが可能になるため、このアプローチを使用するメリットは非常に魅力的です。



ところで、MS SQL Serverの単体テストで単体テストにLINQ2SQLとSMOを使用できる場合、次の基本クラスを使用してデータアクセスレイヤーをテストできます。



コード
 public abstract class DatabaseUnitTest<TContext> where TContext : DataContext { [TestFixtureSetUp] public void FixtureSetUp() { CreateFolderForTempDatabase(); } [SetUp] public void BeforeTestExecuting() { RestoreDatabaseFromOriginal(); RecreateContext(); } [TestFixtureTearDown] public void FixtureTearDown() { KillDatabase(); } protected string ConnectionString { get { return String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True", TestServerName, TestDatabaseName); } } protected TContext Context { get; private set; } protected string TestDatabaseOriginalName { get { return "Database"; } } protected string ProjectName { get { return "CoolProject"; } } protected void RecreateContext() { Context = (TContext) Activator.CreateInstance(typeof(TContext), ConnectionString); } private string FolderForTempDatabase { get { return String.Format(@"R:\{0}.DatabaseTests\", ProjectName); } } private string TestDatabaseName { get { return FolderForTempDatabase + ProjectName + ".Tests"; } } private string TestDatabaseOriginalFileName { get { return Path.Combine(TestDatabaseDirectory, TestDatabaseOriginalName + ".mdf"); } } private string TestDatabaseFileName { get { return Path.Combine(TestDatabaseDirectory, TestDatabaseName + ".mdf"); } } private void CreateFolderForTempDatabase() { var directory = new DirectoryInfo(FolderForTempDatabase); if(!directory.Exists) { directory.Create(); } } private void RestoreDatabaseFromOriginal() { KillDatabase(); CopyFiles(); AttachDatabase(); } private void KillDatabase() { Server server = Server; SqlConnection.ClearAllPools(); if(server.Databases.Contains(TestDatabaseName)) { server.KillDatabase(TestDatabaseName); } } private void CopyFiles() { new FileInfo(TestDatabaseOriginalFileName).CopyTo(TestDatabaseFileName, true); string logFileName = GetLogFileName(TestDatabaseFileName); new FileInfo(GetLogFileName(TestDatabaseOriginalFileName)).CopyTo(logFileName, true); new FileInfo(TestDatabaseFileName).Attributes = FileAttributes.Normal; new FileInfo(logFileName).Attributes = FileAttributes.Normal; } private void AttachDatabase() { Server server = Server; if(!server.Databases.Contains(TestDatabaseName)) { server.AttachDatabase(TestDatabaseName, new StringCollection {TestDatabaseFileName, GetLogFileName(TestDatabaseFileName)}); } } private static string GetLogFileName(string databaseFileName) { return new Regex(".mdf$", RegexOptions.IgnoreCase).Replace(databaseFileName, "_log.ldf"); } private static Server Server { get { return new Server(TestServerName); } } private static string TestServerName { get { return "."; } } private static string TestDatabaseDirectory { get { var debugDirectory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); DirectoryInfo binDirectory = debugDirectory.Parent; DirectoryInfo testProjectDirectory; if(binDirectory == null || (testProjectDirectory = binDirectory.Parent) == null) { throw new Exception(""); } return Path.Combine(testProjectDirectory.FullName, "Database"); } } }
      
      







データベースとの相互作用のテストを使用すると、次のようになります。



 [TestFixture] public class ObjectFinderTest : DatabaseUnitTest<DatabaseDataContext> { [Test] public void Test_NullWhenObjectNotExists() { //arrange var fakeIdentifier = 0; var finder = new ObjectFinder(fakeIdentifier, ConnectionString); //act var foundObject = finder.Find(); //assert Assert.That(foundObject, Is.Null); } [Test] public void Test_SuccessfullyFound() { //arrange var insertedObject = ObjectsFactory.Create(); Context.Objects.InsertOnSubmit(insertedObject); Context.SubmitChanges(); var finder = new ObjectFinder(insertedObject.Id, ConnectionString); //act var foundObject = finder.Find(); //assert Assert.That(foundObject.Id, Is.EqualTo(insertedObject.Id)); Assert.That(foundObject.Property, Is.EqualTo(insertedObject.Property)); } }
      
      







出来上がり! データベースアクセスレイヤーをテストする機会を得ました。



ケース2. ASP.NET MVC WebAPI



WebAPIをテストするときの質問の1つは、特定のURLに要求を送信するときに適切な引数を使用して適切なコントローラーで適切なメソッドを呼び出すことをテストできるように、単体テストを構築する方法です。 コントローラーの責任がシステムの対応するクラスまたはコンポーネントにのみ呼び出しをリダイレクトすることであると想定する場合、コントローラーのテストに関する質問に対する答えは、テストを開始する前に、コントローラーが必要なHTTPリクエストを送信できる環境を動的に構築するという事実に還元されますモックを使用して、構成されたルーティングの正確性を確認します。 同時に、IISを使用してテスト環境を展開したくありません。 理想的には、各テストを実行する前にテスト環境を作成する必要があります。 これにより、ユニットテストを互いに十分に分離できます。 IISでは、この点で非常に困難です。



幸いなことに、.NET Framework 4.5のリリースにより、ルーティングのテストの問題を非常に簡単に解決できるようになりました。 たとえば、次のクラスを使用します(UnityはDIコンテナとして使用されます)。



コード
 public abstract class AbstractControllerTest<TController> where TController : ApiController { private HttpServer _server; private HttpClient _client; private UnityContainer _unityContainer; [SetUp] public void BeforeTestExecuting() { _unityContainer = new UnityContainer(); var configuration = new HttpConfiguration(); WebApiConfig.Register(configuration, new IoCContainer(_unityContainer)); _server = new HttpServer(configuration); _client = new HttpClient(_server); Register<TController>(); RegisterConstructorDependenciesAndInjectionProperties(typeof(TController)); } [TearDown] public void AfterTestExecuted() { _client.Dispose(); _server.Dispose(); _unityContainer.Dispose(); } protected TestHttpRequest CreateRequest(string url) { return new TestHttpRequest(_client, url); } protected void Register<T>(T instance) { Register(typeof(T), instance); } private void Register(Type type, object instance) { _unityContainer.RegisterInstance(type, instance); } private void Register<T>() { _unityContainer.RegisterType<T>(); } private void RegisterConstructorDependenciesAndInjectionProperties(Type controllerType) { var constructors = controllerType.GetConstructors(); var constructorParameters = constructors .Select(constructor => constructor.GetParameters()) .SelectMany(constructorParameters => constructorParameters); foreach (var constructorParameter in constructorParameters) { RegisterMockType(constructorParameter.ParameterType); } var injectionProperties = controllerType.GetProperties() .Where(info => info.GetCustomAttributes(typeof(DependencyAttribute), false) .Any()); foreach (var property in injectionProperties) { RegisterMockType(property.PropertyType); } } private void RegisterMockType(Type parameterType) { dynamic mock = Activator.CreateInstance(typeof(Mock<>).MakeGenericType(parameterType), new object[] { MockBehavior.Default }); Register(parameterType, mock.Object); } }
      
      





 public sealed class TestHttpRequest { private readonly HttpClient _client; private readonly Uri _uri; public TestHttpRequest(HttpClient client, string url) { _client = client; _uri = new Uri(new Uri("http://can.be.anything/"), url); } public void AddHeader(string header, object value) { _client.DefaultRequestHeaders.Add(header, value.ToString()); } public HttpResponseMessage Get() { return _client.GetAsync(_uri).Result; } public HttpResponseMessage Post(byte[] content) { return _client.PostAsync(_uri, new ByteArrayContent(content)).Result; } public HttpResponseMessage Put(byte[] content) { return _client.PutAsync(_uri, new ByteArrayContent(content)).Result; } public HttpResponseMessage Head() { var message = new HttpRequestMessage(HttpMethod.Head, _uri); return _client.SendAsync(message).Result; } }
      
      







これで、これらのクラスを使用して、特定のPlatformクラスのシリアル化された日付とオブジェクトを返す架空のコントローラーをテストできます。



 [TestFixture] public class MyControllerTest : AbstractControllerTest<MyController> { private MockRepository _mocks; protected override void OnSetup() { _mocks = new MockRepository(MockBehavior.Strict); } [Test] public void Test_GetDates() { //arrange var january = new DateTime(2013, 1, 1); var february = new DateTime(2013, 2, 1); var repositoryMock = _mocks.Create<IRepository>(); repositoryMock .Setup(r => r.GetDates()) .Returns(new[] {january, february}); Register(repositoryMock.Object); //act var dates = ExecuteGetRequest<DateTime[]>("/api/build-dates"); //assert _mocks.VerifyAll(); Assert.That(dates, Is.EquivalentTo(new[] { january, february })); } [Test] public void Test_GetPlatforms() { //arrange var platform1 = new Platform {Id=1, Name = "1"}; var platform2 = new Platform {Id=2, Name = "2"}; var repositoryMock = _mocks.Create<IRepository>(); repositoryMock .Setup(r => r.GetPlatforms()) .Returns(new[] { platform1, platform2 }); Register(repositoryMock.Object); //act var platforms = ExecuteGetRequest<Platform[]>("/api/platforms"); //assert _mocks.VerifyAll(); Assert.That(platforms, Is.EquivalentTo(new[] { platform1, platform2 })); } private T ExecuteGetRequest<T>(string uri) { var request = CreateRequest(url); var response = request.Get(); T result; response.TryGetContentValue(out result); return result; } }
      
      







それだけです。 コントローラーは単体テストでカバーされています。



ケース3.その他



そして、他のすべてで、それは非常に簡単です。 外部環境と相互作用することなく、純粋なロジックを含むクラスの単体テストの例は、Kent Beck TDDの一般的な文献で提供されている例タイプによるものと実質的に違いはありません。 したがって、ここには特別なトリックはありません。



さらに、プログラムロジックのエラー数を減らすことに加えて、単体テストを使用すると次の利点も得られます。





リストされた「パン」は常に「テスト・ファースト」の原則に従って、常に新鮮でおいしいものであることに注意する価値があります。 要件は変更されましたか? テストを追加し、コードを変更します。 エラーを修正しますか? テストを追加し、コードを変更します。 最も難しいのは、テストの認識を変えることです。 単体テストは、多くの場合、「メイン」コードとは異なる何かとして認識されます。 これは、私の意見では、TDDを完全に使用するための主な障害です。 そして、単体テストとプログラムされた機能が1つの全体の一部であることを認識するために、それを克服しなければなりません。



現在までに、私たちのチームが取り組んでいるプロジェクトには約1,000のユニットテストがあります。 TeamCityでのすべてのテストのビルドおよび起動時間は4分強です。 この記事で説明されているアプローチにより、システムのほぼすべてのレイヤーをテストし、コードの変更と開発を制御できます。 私たちの経験が誰かに役立つことを願っています。




All Articles