実際に自分のデータベースコンテキストを使用するASP.NET Coreコントローラの単体テストを作成するにはどうすればよいですか?

asp.net-core asp.net-mvc c# entity-framework-core unit-testing

質問

実際の ASP.NETコアコントローラの動作に対して優れた単体テストを作成する方法に関する情報はほとんどないようです。この作業を実際に行う方法に関するガイダンスはありますか?

人気のある回答

私は今かなりうまくいくようなシステムを持っているので、私はそれを共有し、それが他の誰かを助けてくれないかどうかを考えました。 Entity Frameworkドキュメンテーションには実際に有用な記事があります。しかし、ここで私はそれを実際の作業アプリケーションに組み込んでいます。

1.ソリューションにASP.NETコアWebアプリケーションを作成する

あなたが始めるのを助ける素晴らしい記事がたくさんあります。基本的なセットアップとスキャフォールディングのドキュメントは非常に役に立ちます。この目的のために、ApplicationDbContextがEntityFrameworkで自動的に動作するように、個々のユーザーアカウントを持つWebアプリケーションを作成する必要があります。

1a。コントローラーを足場にする

ドキュメントに記載されている情報を使用して、基本的なCRUDアクションを持つシンプルなコントローラを作成します。

2.単体テスト用に別のクラスライブラリを作成する

ソリューションでは、新しい.NET Core Libraryを作成し、新しく作成したWebアプリケーションを参照します。私の例では、私が使用しているモデルはCompanyと呼ばれ、 CompaniesController使用していCompaniesController

2a。テストライブラリに必要なパッケージを追加する

このプロジェクトでは、私が使うのxUnitを私のテストランナー、と部品番号のオブジェクト、およびモックのためFluentAssertionsをより有意義なアサーションを作成します。 NuGet Package Managerおよび/またはConsoleを使用して、これらの3つのライブラリをプロジェクトに追加します。 [ Show PrereleaseShow PrereleaseチェックボックスをShow Prereleaseして検索する必要があります。

また、EntityFrameworkの新しいSqlite-InMemoryデータベースオプションを使用するには、2つのパッケージが必要です。 これは秘密のソースです。 NuGetのパッケージ名のリストを以下に示します。

  • Microsoft.Data.Sqlite
  • Microsoft.EntityFramework Core .InMemory [重点追加]
  • Microsoft.EntityFramework コア .Sqlite [重点追加]

3.テストフィクスチャの設定

先ほど触れた記事では、テストを実行できるメモリ内のリレーショナルデータベースとして動作するようにSqliteをセットアップする簡単で美しい方法があります。

単位テストメソッドを作成して、各メソッドにデータベースの新しいクリーンコピーが作成されるようにします。上の記事では、これを一回限り行う方法を説明します。私のフィクスチャーをできるだけDRYにする方法は次のとおりです。

3a。同期コントローラの動作

私はアレンジ/アクト/アサートモデルを使ってテストを書くことができる以下のメソッドを書いた。各ステージはテストのパラメータとして機能する。以下は、メソッドのコードと、それが参照するTestFixture関連するクラスプロパティです。最後に、コードを呼び出すための例を示します。

public class TestFixture {
    public SqliteConnection ConnectionFactory() => new SqliteConnection("DataSource=:memory:");

    public DbContextOptions<ApplicationDbContext> DbOptionsFactory(SqliteConnection connection) =>
        new DbContextOptionsBuilder<ApplicationDbContext>()
        .UseSqlite(connection)
        .Options;

    public Company CompanyFactory() => new Company {Name = Guid.NewGuid().ToString()};

    public void RunWithDatabase(
        Action<ApplicationDbContext> arrange,
        Func<ApplicationDbContext, IActionResult> act,
        Action<IActionResult> assert)
    {
        var connection = ConnectionFactory();
        connection.Open();

        try
        {
            var options = DbOptionsFactory(connection);

            using (var context = new ApplicationDbContext(options))
            {
                context.Database.EnsureCreated();
                // Arrange
                arrange?.Invoke(context);
            }

            using (var context = new ApplicationDbContext(options))
            {
                // Act (and pass result into assert)
                var result = act.Invoke(context);
                // Assert
                assert.Invoke(result);
            }
        }
        finally
        {
            connection.Close();
        }
    }
    ...
}

ここでは、 CompaniesController Createメソッドをテストするためのコードを呼び出す方法を示しCompaniesController (私は式をそのままにしておくのに役立つパラメータ名を使用しますが、厳密にそれらを必要としません)。

    [Fact]
    public void Get_ReturnsAViewResult()
    {
        _fixture.RunWithDatabase(
            arrange: null,
            act: context => new CompaniesController(context, _logger).Create(), 
            assert: result => result.Should().BeOfType<ViewResult>()
        );
    }

私のCompaniesControllerクラスにはロガーが必要です。私はMoqでモックアップし、TestFixtureに変数として保存します。

3b。非同期コントローラアクション

もちろん、組み込みのASP.NETコアアクションの多くは非同期です。これらの構造体を使用するには、以下のメソッドを記述しました。

public class TestFixture {
    ...
    public async Task RunWithDatabaseAsync(
        Func<ApplicationDbContext, Task> arrange,
        Func<ApplicationDbContext, Task<IActionResult>> act,
        Action<IActionResult> assert)
    {
        var connection = ConnectionFactory();
        await connection.OpenAsync();

        try
        {
            var options = DbOptionsFactory(connection);

            using (var context = new ApplicationDbContext(options))
            {
                await context.Database.EnsureCreatedAsync();
                if (arrange != null) await arrange.Invoke(context);
            }

            using (var context = new ApplicationDbContext(options))
            {
                var result = await act.Invoke(context);
                assert.Invoke(result);
            }
        }
        finally
        {
            connection.Close();
        }
    }
}

これはほとんど同じですが、非同期メソッドと待ち時間の設定だけです。以下に、これらのメソッドを呼び出す例を示します。

    [Fact]
    public async Task Post_WhenViewModelDoesNotMatchId_ReturnsNotFound()
    {
        await _fixture.RunWithDatabaseAsync(
            arrange: async context =>
            {
                context.Company.Add(CompanyFactory());
                await context.SaveChangesAsync();
            },
            act: async context => await new CompaniesController(context, _logger).Edit(1, CompanyFactory()),
            assert: result => result.Should().BeOfType<NotFoundResult>()
        );
    }

3c。データによる非同期アクション

もちろん、テストの段階の間でデータを前後に渡す必要があることもあります。ここで私があなたがそれを行うことを可能にする方法があります:

public class TestFixture {
    ...
    public async Task RunWithDatabaseAsync(
        Func<ApplicationDbContext, Task<dynamic>> arrange,
        Func<ApplicationDbContext, dynamic, Task<IActionResult>> act,
        Action<IActionResult, dynamic> assert)
    {
        var connection = ConnectionFactory();
        await connection.OpenAsync();

        try
        {
            object data;
            var options = DbOptionsFactory(connection);

            using (var context = new ApplicationDbContext(options))
            {
                await context.Database.EnsureCreatedAsync();
                data = arrange != null 
                    ? await arrange?.Invoke(context) 
                    : null;
            }

            using (var context = new ApplicationDbContext(options))
            {
                var result = await act.Invoke(context, data);
                assert.Invoke(result, data);
            }
        }
        finally
        {
            connection.Close();
        }
    }
}

もちろん、このコードの使い方の例は次のとおりです。

    [Fact]
    public async Task Post_WithInvalidModel_ReturnsModelErrors()
    {
        await _fixture.RunWithDatabaseAsync(
            arrange: async context =>
            {
                var data = new
                {
                    Key = "Name",
                    Message = "Name cannot be null",
                    Company = CompanyFactory()
                };
                context.Company.Add(data.Company);
                await context.SaveChangesAsync();
                return data;
            },
            act: async (context, data) =>
            {
                var ctrl = new CompaniesController(context, _logger);
                ctrl.ModelState.AddModelError(data.Key, data.Message);
                return await ctrl.Edit(1, data.Company);
            },
            assert: (result, data) => result.As<ViewResult>()
                .ViewData.ModelState.Keys.Should().Contain((string) data.Key)
        );
    }

結論

私は本当にこれが誰かがC#とASP.NET Coreのすばらしい新しいもので立ち上がるのに役立つことを願っています。質問、批判、または提案がある場合は、私に教えてください!私はまだこれも新しいので、建設的なフィードバックは私にとって非常に貴重です!



Related

ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ
ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ