如何為實際使用我的數據庫上下文的ASP.NET核心控制器編寫單元測試?

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

似乎沒有關於如何為實際的 ASP.NET核心控制器操作編寫好的單元測試的信息。有關如何使這項工作真實的任何指導?

熱門答案

我有一個似乎現在工作得很好的系統,所以我想我會分享它,看看它是否對其他人沒有幫助。 實體框架文檔中有一篇非常有用的文章指明了方向。但這是我如何將其納入實際的工作應用程序。

1.在解決方案中創建ASP.NET Core Web App

有很多很棒的文章可以幫助你入門。基本設置和腳手架的文檔非常有用。為此,您需要創建一個包含個人用戶帳戶的Web應用程序,以便您的ApplicationDbContext設置為自動使用EntityFramework。

1A。腳手架一個控制器

使用文檔中包含的信息創建一個具有基本CRUD操作的簡單控制器。

2.為單元測試創建單獨的類庫

在您的解決方案中,創建一個新的.NET Core Library並引用您新創建的Web應用程序。在我的示例中,我正在使用的模型稱為Company ,它使用CompaniesController

2A。將必要的包添加到測試庫中

對於這個項目,我使用xUnit作為我的測試運行器,使用Moq模擬對象,使用FluentAssertions來創建更有意義的斷言。使用NuGet Package Manager和/或Console將這三個庫添加到項目中。您可能需要選中“ Show Prerelease複選框來搜索它們。

您還需要一些包來使用EntityFramework的新Sqlite-InMemory數據庫選項。 這是秘訣。以下是NuGet上的軟件包名稱列表:

  • Microsoft.Data.Sqlite
  • Microsoft.EntityFramework Core .InMemory [強調添加]
  • Microsoft.EntityFramework Core .Sqlite [強調添加]

3.設置您的測試夾具

根據我之前提到的文章 ,有一種簡單,美觀的方法可以將Sqlite設置為內存中的關係數據庫,您可以對其進行測試。

您將要編寫單元測試方法,以便每個方法都有一個新的,乾淨的數據庫副本。上面的文章向您展示瞭如何一次性完成這項工作。以下是我將夾具設置為盡可能乾燥的方法。

3A。同步控制器動作

我編寫了以下方法,允許我使用Arrange / Act / Assert模型編寫測試,每個階段在我的測試中充當參數。下面是方法的代碼以及它引用的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方法的樣子(我使用參數名稱來幫助我保持表達式的直接,但是你並不嚴格需要它們):

    [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 Core操作都是異步的。為了使用這個結構,我寫了下面的方法:

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合法嗎? 是的,了解原因