ASP.NET Core
為何要使用Testcontainers?
筆者先前在實務上運行在整合測試的時候,先前都是用localDB來進行做整合測試,再來整合測試部分建置環境的要求會比較多,必須要建置資料庫和安裝所需的工具。
所以這時候才開始嘗試接觸用Testcontainers部分,這一次先實驗用EFCore來實驗看看,在看未來抽空寫Dapper部分,因為在企業很多運行的系統移轉多數都是從ADO.net DataTable慢慢演進使用Dapper來做資料存取方案,這一次先嘗試用EF Core的做法來,未來當做是一個使用的評估選擇。
所使用環境實驗
- Windows 10 Home 19045.4894
- Docker
- ASP.net Core
- Visual Studio 2022
快速建立API
建立空白的ASP.net Core WebAPI並在專案編輯檔案,查看一下並設置False…
為何要設置False筆者在建置EF Core有遇到 System.Globalization.CultureNotFoundException: Only the invariant culture is supported in globalization-invariant mode異常。
<InternalsVisibleTo Include="CategoryAPI" />
<InvariantGlobalization>false</InvariantGlobalization>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
<InternalsVisibleTo Include="CategoryAPI" />
</ItemGroup>
</Project>
接者WebApi需要套件
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
開始建立Model/Category.cs
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
建置CategoryDbContext
public class CategoryDbContext:DbContext
{
public CategoryDbContext(DbContextOptions<CategoryDbContext> options):base(options)
{
}
public DbSet<Category> Categories { get; set; }
}
在方案底下的Controller底下用滑鼠右鍵加入API控制器並使用Entity Framework執行動作API控制器,這樣可以快速建立骨架程式碼。
註記:這個在vs code也是有快速建置程式碼的CRUD骨架,可以由開發者自己微調。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using CategoryAPI.Model;
namespace CategoryAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CategoriesController : ControllerBase
{
private readonly CategoryDbContext _context;
public CategoriesController(CategoryDbContext context)
{
_context = context;
}
// GET: api/Categories
[HttpGet]
public async Task<ActionResult<IEnumerable<Category>>> GetCategories()
{
return await _context.Categories.ToListAsync();
}
// GET: api/Categories/5
[HttpGet("{id}")]
public async Task<ActionResult<Category>> GetCategory(int id)
{
var category = await _context.Categories.FindAsync(id);
if (category == null)
{
return NotFound();
}
return category;
}
// PUT: api/Categories/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutCategory(int id, Category category)
{
if (id != category.Id)
{
return BadRequest();
}
_context.Entry(category).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CategoryExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Categories
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Category>> PostCategory(Category category)
{
_context.Categories.Add(category);
await _context.SaveChangesAsync();
return CreatedAtAction("GetCategory", new { id = category.Id }, category);
}
// DELETE: api/Categories/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCategory(int id)
{
var category = await _context.Categories.FindAsync(id);
if (category == null)
{
return NotFound();
}
_context.Categories.Remove(category);
await _context.SaveChangesAsync();
return NoContent();
}
private bool CategoryExists(int id)
{
return _context.Categories.Any(e => e.Id == id);
}
}
}
//Program.cs
public partial class Program { } // 為了 WebApplicationFactory 以便在測試專案當中能夠使用Program
接者建立所需的測試專案要的套件
- FluentAssertions
- Microsoft.AspNetCore.Mvc.Testing
- Microsoft.Extensions.DependencyInjection
- Testcontainers.MsSql
並加入專案參考CategoryApi
。
使用XUnit並用TestContainers來做整合測試
依據測試容器開始寫程式,建立一個CategoryApiApplicationFactory
類別來實作我們的WebApplicationFactory<T>
類別所在的類別,在建構子當中設置SQL SERVER容器設定。
public class CategoryApiApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private const string Database = "master";
private const string Username = "sa";
private const string Password = "yourStrong(!)Password";
private const ushort MsSqlPort = 1433;
private readonly IContainer _mssqlContainer;
public CategoryApiApplicationFactory()
{
// Initialize the SQL Server container
_mssqlContainer = new ContainerBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2019-latest")
.WithPortBinding(MsSqlPort, true)
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("MSSQL_SA_PASSWORD", Password)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(MsSqlPort))
.Build();
}
}
最後再補上資料庫的路徑與連線路徑和建立資料庫,確保讓整合測試可以跑。
public class CategoryApiApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private const string Database = "master";
private const string Username = "sa";
private const string Password = "yourStrong(!)Password";
private const ushort MsSqlPort = 1433;
private readonly IContainer _mssqlContainer;
public CategoryApiApplicationFactory()
{
// Initialize the SQL Server container
_mssqlContainer = new ContainerBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2019-latest")
.WithPortBinding(MsSqlPort, true)
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("MSSQL_SA_PASSWORD", Password)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(MsSqlPort))
.Build();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var host = _mssqlContainer.Hostname;
var port = _mssqlContainer.GetMappedPublicPort(MsSqlPort);
builder.ConfigureServices(services =>
{
services.RemoveAll(typeof(DbContextOptions<CategoryDbContext>));
services.AddDbContext<CategoryDbContext>(options =>
options.UseSqlServer(
$"Server={host},{port};Database={Database};User Id={Username};Password={Password};TrustServerCertificate=True"));
// Build the service provider
var serviceProvider = services.BuildServiceProvider();
// Ensure the database is created
using (var scope = serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<CategoryDbContext>();
// Ensure the database is created asynchronously
dbContext.Database.EnsureCreatedAsync().Wait();
}
});
}
public async Task InitializeAsync()
{
await _mssqlContainer.StartAsync();
}
public async Task DisposeAsync()
{
await _mssqlContainer.DisposeAsync();
}
}
using FluentAssertions;
using System.Net.Http.Json;
namespace CategoryAPITests.Categories
{
public class CategoryApiTests : IClassFixture<CategoryApiApplicationFactory>
{
private HttpClient _client;
public CategoryApiTests(CategoryApiApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetCategories_ShouldReturnSuccess()
{
var response = await _client.GetAsync("/api/categories");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
content.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task CreateCategory_ShouldAddNewCategory()
{
var newCategory = new
{
Name = "New Category",
Description = "This is a test category"
};
var response = await _client.PostAsJsonAsync("/api/categories", newCategory);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("New Category");
}
}
}
最後運行整合測試。
總結:未來自己若遇到EF Core的系統,未來在寫整合測試的時候,也是一個這樣搭配組合備選方案的評估,畢竟容器化已經簡化了很多處理過程,不用自己準備環境和建置環境之類。
元哥的筆記