Loading...
Loading...
ASP.NET Core 整合測試的專門技能。當需要測試 Web API 端點、HTTP 請求/回應、中介軟體、依賴注入時使用。涵蓋 WebApplicationFactory、TestServer、HttpClient 測試、記憶體資料庫配置等。 Keywords: integration testing, 整合測試, web api testing, WebApplicationFactory, TestServer, HttpClient testing, controller testing, endpoint testing, 端點測試, RESTful API testing, Microsoft.AspNetCore.Mvc.Testing, CreateClient, ConfigureWebHost, AwesomeAssertions.Web, Be200Ok, Be404NotFound, middleware testing, 中介軟體測試, dependency injection testing
npx skill4agent add kevintsengtw/dotnet-testing-agent-skills dotnet-testing-advanced-aspnet-integration-testingWebApplicationFactory<T>TestServer<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<PackageReference Include="AwesomeAssertions.Web" Version="1.9.6" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.8" />⚠️ 重要提醒:使用時,必須安裝AwesomeAssertions,而非AwesomeAssertions.Web。FluentAssertions.Web
將兩個以上的類別做整合,並且測試它們之間的運作是不是正確的,測試案例一定是跨類別物件的
會使用到外部資源,例如資料庫、外部服務、檔案、需要對測試環境進行特別處理等
| 測試類型 | 測試範圍 | 執行速度 | 維護成本 | 建議比例 |
|---|---|---|---|---|
| 單元測試 | 單一類別/方法 | 很快 | 低 | 70% |
| 整合測試 | 多個元件 | 中等 | 中等 | 20% |
| 端對端測試 | 完整流程 | 慢 | 高 | 10% |
public class BasicIntegrationTest : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicIntegrationTest(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Get_首頁_應回傳成功()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/");
// Assert
response.EnsureSuccessStatusCode();
}
}public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>
where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// 移除原本的資料庫設定
services.RemoveAll(typeof(DbContextOptions<AppDbContext>));
// 加入記憶體資料庫
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("TestDatabase");
});
// 替換外部服務為測試版本
services.Replace(ServiceDescriptor.Scoped<IEmailService, TestEmailService>());
});
// 設定測試環境
builder.UseEnvironment("Testing");
}
}response.Should().Be200Ok(); // HTTP 200
response.Should().Be201Created(); // HTTP 201
response.Should().Be204NoContent(); // HTTP 204
response.Should().Be400BadRequest(); // HTTP 400
response.Should().Be404NotFound(); // HTTP 404
response.Should().Be500InternalServerError(); // HTTP 500[Fact]
public async Task GetShipper_當貨運商存在_應回傳成功結果()
{
// Arrange
await CleanupDatabaseAsync();
var shipperId = await SeedShipperAsync("順豐速運", "02-2345-6789");
// Act
var response = await Client.GetAsync($"/api/shippers/{shipperId}");
// Assert
response.Should().Be200Ok()
.And
.Satisfy<SuccessResultOutputModel<ShipperOutputModel>>(result =>
{
result.Status.Should().Be("Success");
result.Data.Should().NotBeNull();
result.Data!.ShipperId.Should().Be(shipperId);
result.Data.CompanyName.Should().Be("順豐速運");
result.Data.Phone.Should().Be("02-2345-6789");
});
}// ❌ 傳統方式 - 冗長且容易出錯
response.IsSuccessStatusCode.Should().BeTrue();
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SuccessResultOutputModel<ShipperOutputModel>>(content,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
result.Should().NotBeNull();
result!.Status.Should().Be("Success");
// ✅ 使用 Satisfy<T> - 簡潔且直觀
response.Should().Be200Ok()
.And
.Satisfy<SuccessResultOutputModel<ShipperOutputModel>>(result =>
{
result.Status.Should().Be("Success");
result.Data!.CompanyName.Should().Be("測試公司");
});// ❌ 傳統方式
var createParameter = new ShipperCreateParameter { CompanyName = "測試公司", Phone = "02-1234-5678" };
var jsonContent = JsonSerializer.Serialize(createParameter);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/shippers", content);
// ✅ 現代化方式
var createParameter = new ShipperCreateParameter { CompanyName = "測試公司", Phone = "02-1234-5678" };
var response = await client.PostAsJsonAsync("/api/shippers", createParameter);// ❌ 傳統方式
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SuccessResultOutputModel<ShipperOutputModel>>(responseContent,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
// ✅ 現代化方式
var result = await response.Content.ReadFromJsonAsync<SuccessResultOutputModel<ShipperOutputModel>>();WebApplicationFactory<Program>public class BasicApiControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public BasicApiControllerTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetStatus_應回傳OK()
{
// Act
var response = await _client.GetAsync("/api/status");
// Assert
response.Should().Be200Ok();
}
}public class ServiceStubWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly IExampleService _serviceStub;
public ServiceStubWebApplicationFactory(IExampleService serviceStub)
{
_serviceStub = serviceStub;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IExampleService>();
services.AddScoped(_ => _serviceStub);
});
}
}
public class ServiceDependentControllerTests
{
[Fact]
public async Task GetData_應回傳服務資料()
{
// Arrange
var serviceStub = Substitute.For<IExampleService>();
serviceStub.GetDataAsync().Returns("測試資料");
var factory = new ServiceStubWebApplicationFactory(serviceStub);
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/api/data");
// Assert
response.Should().Be200Ok();
}
}public class FullDatabaseWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// 移除原本的資料庫設定
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// 加入記憶體資料庫
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("TestDatabase");
});
// 建立資料庫並加入測試資料
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Database.EnsureCreated();
});
}
}public abstract class IntegrationTestBase : IDisposable
{
protected readonly CustomWebApplicationFactory Factory;
protected readonly HttpClient Client;
protected IntegrationTestBase()
{
Factory = new CustomWebApplicationFactory();
Client = Factory.CreateClient();
}
protected async Task<int> SeedShipperAsync(string companyName, string phone = "02-12345678")
{
using var scope = Factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var shipper = new Shipper
{
CompanyName = companyName,
Phone = phone,
CreatedAt = DateTime.UtcNow
};
context.Shippers.Add(shipper);
await context.SaveChangesAsync();
return shipper.ShipperId;
}
protected async Task CleanupDatabaseAsync()
{
using var scope = Factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Shippers.RemoveRange(context.Shippers);
await context.SaveChangesAsync();
}
public void Dispose()
{
Client?.Dispose();
Factory?.Dispose();
}
}tests/
├── Sample.WebApplication.UnitTests/ # 單元測試
├── Sample.WebApplication.Integration.Tests/ # 整合測試
│ ├── Controllers/ # 控制器整合測試
│ │ └── ShippersControllerTests.cs
│ ├── Infrastructure/ # 測試基礎設施
│ │ └── CustomWebApplicationFactory.cs
│ ├── IntegrationTestBase.cs # 測試基底類別
│ └── GlobalUsings.cs
└── Sample.WebApplication.E2ETests/ # 端對端測試error CS1061: 'ObjectAssertions' 未包含 'Be200Ok' 的定義| 基礎斷言庫 | 正確的套件 |
|---|---|
| FluentAssertions < 8.0.0 | FluentAssertions.Web |
| FluentAssertions >= 8.0.0 | FluentAssertions.Web.v8 |
| AwesomeAssertions >= 8.0.0 | AwesomeAssertions.Web |
<!-- 正確:使用 AwesomeAssertions 應該安裝 AwesomeAssertions.Web -->
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<PackageReference Include="AwesomeAssertions.Web" Version="1.9.6" />unit-test-fundamentalsnsubstitute-mockingawesome-assertions-guidetestcontainers-database