単位と統合テスト.MOQとXUnitによるネット


テスト.XUnitとMOQによるネット


今日はいくつかの簡単なテストのセットアップと設定をカバーしたい.私が先週作ったポストから私のサンプルプロジェクトを使っているので、このポストを考えてください.ツールのために私は使用されますxUnit and Moq 他のいくつかのnugetパッケージと同様に、Visual Studioで簡単に構成してテストを実行します.私はMacに取り組んでいますが、オペレーティングシステムは問題ではありません.
我々は、我々が我々のコードをテストしなければならないということを知っています.私が働いている会社は何年もの間、ひどく悪かったですが、最近はテスト戦略の開発に取り組んでいます.モックとXUnitはかなり簡単にテストを行い、我々はいくつかのバグを早期にキャッチすることができます.私も、テストはおそらく楽しくなる可能性があります…?

ので、多分楽しみはストレッチですが、それはまだ重要です.それで、始めましょう!

コード


この例のすべてのソースコードをhttps://github.com/MelodicDevelopment/example-dotnet-api-cqrs .

テスト


DOTNET API CQRSテストプロジェクトには、単体テストと同様に統合を行うための(簡単な)例が含まれています.これは、私たちの特定のdapper/cqrsパターンに対して、非常に適しています.
注意を取る1つのことは、実際のデータベースに対して実行するテストを構成したことです.あなたが好きならば、あなたはmockデータをすることができます、しかし、我々は我々のCCIデータベースに対して我々の質問と命令を走らせるほうを選びました.これは、我々のコードを破るだろうデータベースの変更が発生していないことを確認するためのテストの追加ボーナスを与えます.クエリはデータベースに対して予想通りに実行されますが、コマンドはすべてロールバックされ、実際にデータベースに何もコミットされません.これを実現するために、Detnet API CQRSからDBContextに基づいたTestDDBContextを作成しました.データプロジェクト.TestDDBContextはDBContextクラスのほとんどの仮想メソッドを拡張して上書きします.この方法で問い合わせとコマンドを区別することができますし、コマンドに対しては、comamndsをロールバックするための追加設定を行います.これはコマンドを実行しようとしますが、実際にデータベースへの変更をコミットしません.このようにして、実際にデータベースに影響を与えることなく、コマンド内でエラーが発生したかどうかを確認します.重複するキーエラーやユニークなキーの例外など、データベースがスローされる例外はすべてキャッチできます.

DOTNET API CQRS。テスト/ testdbcontext。cs


using System;
using System.Collections.Generic;
using System.Data;
using dotnet_api_cqrs.data;

namespace dotnet_api_cqrs.tests
{
    public class TestDbContext : DbContext
    {
        private readonly bool _noCommit;
        private bool _isQuery;

        public TestDbContext(string connectionString, bool noCommit = false) : base(connectionString)
        {
            _noCommit = noCommit;
        }

        public override IEnumerable<T> Query<T>(string query, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null)
        {
            _isQuery = true;
            return base.Query<T>(query, param, commandType, transaction);
        }

        public override T QueryFirst<T>(string query, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null)
        {
            _isQuery = true;
            return base.QueryFirst<T>(query, param, commandType, transaction);
        }

        public override int InsertSingle(string sql, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null, int? timeout = null)
        {
            _isQuery = false;
            return base.InsertSingle(sql, param, commandType, transaction, timeout);
        }

        public override int Command(string sql, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null, int? timeout = null)
        {
            _isQuery = false;
            return base.Command(sql, param, commandType, transaction, timeout);
        }

        public override T Transaction<T>(Func<IDbTransaction, T> query)
        {
            if (!_noCommit || _isQuery) {
                _isQuery = false;
                return base.Transaction(query);
            } else {
                _isQuery = false;

                using var connection = Connection;
                using var transaction = BeginTransaction();

                try {
                    var result = query(transaction);
                } catch (Exception) {
                    throw;
                }

                transaction.Rollback();

                return default;
            }
        }

        public override void Transaction(Action<IDbTransaction> query)
        {
            if (!_noCommit || _isQuery) {
                _isQuery = false;
                base.Transaction(query);
            } else {
                _isQuery = false;

                using var connection = Connection;
                using var transaction = BeginTransaction();

                try {
                    query(transaction);
                } catch (Exception) {
                    throw;
                }

                transaction.Rollback();
            }
        }

        ~TestDbContext()
        {
            Dispose();
        }
    }
}
それで、我々のテストは本当の生のデータベースに反対します.しかし、我々の生産データベース.我々はライブそれをテストしているが、実際に住んでいない….そして、我々はコミットしません.…それはすべて正しく動作しますか?私たちのために働いているようです.

必要なNugetパッケージ


Visual StudioのNugetパッケージマネージャーを見ると、どのパッケージが必要かを見ることができますが、簡単に以下のようになります.
  • 模擬
  • X単位
  • XUnitランナー.ビジュアルスタジオ
  • マイクロソフト.アスピネット.MVCテスト
  • マイクロソフト.アスピネット.テストス
  • マイクロソフト.ネットテスト.SDK
  • 単体テスト


    単体テストについては、どのように我々の問い合わせと1つのコマンドのテストを見ていきます.正しいテストDBContextを使用していることを確認するには、すべてのユニットテストが拡張された基本クラスを作成しました.これはTestBaseと呼ばれます.それはtestdbcontextを知らせて、nocommit旗で通過するのを許します.このフラグは、コマンドがデータベースにコミットされないようにする.

    DOTNET API CQRS。テスト/テストベース。cs


    using dotnet_api_cqrs.contracts.data;
    
    namespace dotnet_api_cqrs.tests
    {
        public abstract class TestBase
        {
            private readonly bool _noCommit;
    
            protected readonly IDbContext TestDbContext;
    
            public TestBase(bool noCommit = false)
            {
                _noCommit = noCommit;
    
                TestDbContext = new TestDbContext($"use-configuration-to-get-connection-string", _noCommit);
            }
        }
    }
    

    DOTNET API CQRS。テスト/データ/ブック/ bookquerytest。cs


    using dotnet_api_cqrs.data.Queries.Book;
    using Xunit;
    
    namespace dotnet_api_cqrs.tests.Data.Book
    {
        public class BookQueryTests : TestBase
        {
            [Fact]
            public void GetAllBooksTest()
            {
                var query = new GetAllBooksQuery();
                var results = query.Execute(TestDbContext);
    
                Assert.NotEmpty(results);
            }
    
            [Theory]
            [InlineData(1)]
            public void GetBookByIDTest(int bookID)
            {
                var query = new GetBookQuery(bookID);
                var results = query.Execute(TestDbContext);
    
                Assert.NotNull(results);
            }
    
    
            [Theory]
            [InlineData(1)]
            public void GetBookForAuthorTest(int authorID)
            {
                var query = new GetBooksForAuthorQuery(authorID);
                var results = query.Execute(TestDbContext);
    
                Assert.NotNull(results);
            }
        }
    }
    
    
    BookQueryTestで.csファイルを実行できる複数のテストが表示されます.これらは、DOTNET API CQRSで見つけることができる我々の質問のすべての3つをテストする2つの単純なテストです.データプロジェクト.XUnitはカップルメソッドの属性を考慮します.最初のものはFact そして、これは単にパラメータなしでテストを実行します.The Theory 属性を指定すると、InlineData . にInlineData 属性を引数に渡すことができます.この作業はGetBookByIDTest テスト方法.再び、これは非常に単純なテストです、そして、我々はちょうど検索する本のIDを示す1を渡します.これらのテストは、はるかに複雑でテストされ、より複雑なテストデータを使用してさまざまなシナリオをテストできます.我々は時間のためにここにそれを取得しませんが、そのドキュメントをチェックアウトし、いくつかのGoogle検索を行ってください.より複雑で厳格な単体テストを書くのに役立つ良いものがたくさんあります.

    MoQを使用してサービスをテストする


    MoQは非常に簡単にセットアップをあなたのユニットテスト内で使用できる模擬オブジェクトをすることができます.別の簡単で基本的な例については、BookServiceTestがあります.csファイル.これは再びTestBaseクラスを拡張し、moqを使用して、ブックサービスがコンストラクタ引数として期待しているIBookFacadeの模擬オブジェクトを作成します.この例では、iBookFacadeのmockを作成し、getBooksメソッドを呼び出すときにいくつかのテストデータを渡すように設定します.この方法では、ブックサービスがファサードのGetBooksメソッドを呼び出すときにいくつかのテストデータを取得し、我々が持っているビジネスロジックを実行します.その後、結果をテストすることができます.

    DOTNET API CQRS。テスト/サービス/ bookServicests。cs


    using dotnet_api_cqrs.contracts.data;
    using dotnet_api_cqrs.data;
    using dotnet_api_cqrs.services;
    using Moq;
    using Xunit;
    
    namespace dotnet_api_cqrs.tests.Services
    {
        public class BookServicesTests : TestBase
        {
            [Fact]
            public void GetAllBooksTest()
            {
                var mockFacade = new Mock<IBookFacade>();
                mockFacade.Setup(repo => repo.GetBooks())
                    .Returns((context, transaction) => {
                        return TestData.Books;
                    });
    
                var bookService = new BookService(TestDbContext, mockFacade.Object);
    
                Assert.NotEmpty(bookService.GetAllBooks());
            }
        }
    }
    

    統合テスト


    XUnitとマイクロソフトを使うこともできます.アスピネット.MVC統合テストを実行するためのNuGetパッケージのテストこれらの統合テストは、メモリ内の仮想Webサーバーを設定し、その仮想サーバー上でAPIプロジェクトを実行し、終了点を押すと、APIのエンドポイントからデータベースクエリまたはコマンドにテストし、途中で問題を返します.
    必要な最初の部分はWebApplicationFactoryと呼ばれます.MVCテストパッケージはこの基本クラスを提供し、テストDBContextを使用してデフォルトのIDBContext依存性インジェクションを上書きできるように少しだけ拡張します.

    DOTNET API CQRS。テスト/ API / apiqueryTestApplication Factory。cs


    using dotnet_api_cqrs.contracts.data;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.AspNetCore.TestHost;
    using Microsoft.Extensions.DependencyInjection.Extensions;
    
    namespace dotnet_api_cqrs.tests.Api
    {
        public class ApiQueryTestApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
        {
            protected override void ConfigureWebHost(IWebHostBuilder builder)
            {
                base.ConfigureWebHost(builder);
    
                builder.ConfigureTestServices(services => {
                    services.RemoveAll<IDbContext>();
                    services.TryAddScoped<IDbContext>(sp => new TestDbContext($"use-configuration-to-get-connection-string"));
                });
            }
        }
    }
    
    この特定のファクトリクラスはクエリのテストです.コマンドが実行されているときにテストする別のものがあります.これらは私が理解するのというより単純なテストです、そして、我々はまだ1つのパスでコマンドと質問を走らせるより複雑なビジネス・ロジックをテストするための堅実な戦略に取り組んでいます、しかし、これはとにかくあなたを始めなければなりません.
    さて、BookControllQueryTestファイルを見ると、すべての書籍を返すBookControllerのエンドポイントを1つ以上テストできます.この特定のテストでは、我々は簡単に呼び出しが成功したことをテストしています.

    DOTNET API CQRS。テスト/ API /コントローラ/ BookControllerQueryTest。cs


    using System.Net.Http;
    using System.Threading.Tasks;
    using dotnet_api_cqrs.api;
    using Xunit;
    
    namespace dotnet_api_cqrs.tests.Api.Controllers
    {
        public class BookControllerTests : IClassFixture<ApiQueryTestApplicationFactory<Startup>>
        {
            private readonly ApiQueryTestApplicationFactory<Startup> _factory;
            private readonly HttpClient _httpClient;
    
            public BookControllerTests(ApiQueryTestApplicationFactory<Startup> factory)
            {
                _factory = factory;
                _httpClient = _factory.CreateClient();
            }
    
            [Fact]
            public async Task GetBooks_IsSuccessful()
            {
                var response = await _httpClient.GetAsync($"/api/book");
    
                response.EnsureSuccessStatusCode();
                Assert.True(response.IsSuccessStatusCode);
            }
        }
    }
    

    結論


    また、これはXUnitとMOQを使用してユニットと統合テストのかなり単純な外観だったが、私はそれがあなたがあなたができる最高の製品を作成していることを確認するために独自のコードでより徹底的かつ複雑なテストへの道を始めたことを願っています.
    コメントがあなたの考えならば私に知らせてください.