dotnet-testing-advanced-aspnet-integration-testing
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-advanced-aspnet-integration-testing
Agent 安装分布
Skill 文档
ASP.NET Core æ´å測試æå
é©ç¨æ å¢
æ¬æè½æå°å¦ä½å¨ ASP.NET Core ä¸å»ºç«ææçæ´å測試ï¼ä½¿ç¨ WebApplicationFactory<T> å TestServer æ¸¬è©¦å®æ´ç HTTP è«æ±/åææµç¨ã
é©ç¨å ´æ¯
- Web API ç«¯é»æ¸¬è©¦ï¼é©è RESTful API ç CRUD æä½
- HTTP è«æ±/åæé©èï¼æ¸¬è©¦å®æ´çè«æ±èç管ç·
- ä¸ä»è»é«æ¸¬è©¦ï¼é©è AuthenticationãAuthorizationãLogging ç
- ä¾è³´æ³¨å ¥é©èï¼ç¢ºä¿ DI 容å¨è¨å®æ£ç¢º
- è·¯ç±è¨å®é©èï¼ç¢ºä¿ URL è·¯ç±æ£ç¢ºå°æå°æ§å¶å¨åä½
- 模åç¹«çµæ¸¬è©¦ï¼é©èè«æ±å §å®¹æ£ç¢ºç¹«çµå°æ¨¡å
å¿ è¦å¥ä»¶
<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ã
æ ¸å¿æ¦å¿µ
æ´å測試çå ©ç¨®å®ç¾©
å®ç¾©ä¸ï¼å¤ç©ä»¶å使¸¬è©¦
å°å ©å以ä¸çé¡å¥åæ´åï¼ä¸¦ä¸æ¸¬è©¦å®åä¹éçé使¯ä¸æ¯æ£ç¢ºçï¼æ¸¬è©¦æ¡ä¾ä¸å®æ¯è·¨é¡å¥ç©ä»¶ç
å®ç¾©äºï¼å¤é¨è³æºæ´å測試
æä½¿ç¨å°å¤é¨è³æºï¼ä¾å¦è³æåº«ãå¤é¨æåãæªæ¡ãéè¦å°æ¸¬è©¦ç°å¢é²è¡ç¹å¥èçç
çºä»éº¼éè¦æ´å測試ï¼
- 確ä¿å¤å模çµå¨æ´åéä½å¾ï¼è½å¤ æ£ç¢ºå·¥ä½
- å®å æ¸¬è©¦ç¡æ³æ¶µèçæ´åé»ï¼RoutingãMiddlewareãRequest/Response Pipeline
- WebApplication åäºå¤ªå¤çæ´åèè¨å®ï¼å®å æ¸¬è©¦ç¡æ³ç¢ºèªå°å ¨é¨
- ç¢ºèªæ¯å¦å®åç°å¸¸èçï¼æ¸å°æ´å¤åé¡çç¼ç
測試éåå¡å®ä½
| 測試é¡å | 測試ç¯å | å·è¡é度 | ç¶è·ææ¬ | å»ºè°æ¯ä¾ |
|---|---|---|---|---|
| å®å 測試 | å®ä¸é¡å¥/æ¹æ³ | å¾å¿« | ä½ | 70% |
| æ´å測試 | å¤åå ä»¶ | ä¸ç | ä¸ç | 20% |
| 端å°ç«¯æ¸¬è©¦ | 宿´æµç¨ | æ ¢ | é« | 10% |
ååä¸ï¼ä½¿ç¨ WebApplicationFactory å»ºç«æ¸¬è©¦ç°å¢
åºæ¬ä½¿ç¨æ¹å¼
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();
}
}
èªè¨ WebApplicationFactory
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");
}
}
ååäºï¼ä½¿ç¨ AwesomeAssertions.Web é©è HTTP åæ
HTTP çæ ç¢¼æ·è¨
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
Satisfy å¼·åå¥é©è
[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("測試å
¬å¸");
});
ååä¸ï¼ä½¿ç¨ System.Net.Http.Json ç°¡å JSON æä½
PostAsJsonAsync ç°¡å POST è«æ±
// â å³çµ±æ¹å¼
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);
ReadFromJsonAsync ç°¡ååæè®å
// â å³çµ±æ¹å¼
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>>();
ä¸å層ç´çæ´å測試çç¥
Level 1ï¼ç°¡å®ç WebApi å°æ¡
ç¹è²ï¼
- æ²æè³æåº«ãService è Repository ä¾è³´
- æç°¡å®ãåºæ¬ç WebApi ç¶²ç«å°æ¡
- ç´æ¥ä½¿ç¨
WebApplicationFactory<Program>é²è¡æ¸¬è©¦
測試éé»ï¼
- åå API çè¼¸å ¥è¼¸åºé©è
- HTTP åè©åè·¯ç±æ£ç¢ºæ§
- 模åç¶å®ååºåå
- çæ ç¢¼ååææ ¼å¼é©è
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();
}
}
Level 2ï¼ç¸ä¾ Service ç WebApi å°æ¡
ç¹è²ï¼
- æ²æè³æåº«ï¼ä½æ Service ä¾è³´
- ä½¿ç¨ NSubstitute å»ºç« Service stub
- 卿¸¬è©¦ä¸é ç½®ä¾è³´æ³¨å ¥
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();
}
}
Level 3ï¼å®æ´ç WebApi å°æ¡
ç¹è²ï¼
- 宿´ç Solution æ¶æ§
- å å«ç實çè³æåº«æä½
- ä½¿ç¨ InMemory æçå¯¦æ¸¬è©¦è³æåº«
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();
}
}
CRUD æä½æ¸¬è©¦ç¯ä¾
宿´ç CRUD æä½æ¸¬è©¦ç¨å¼ç¢¼ï¼GETãPOSTãé©èé¯èª¤ãéåæ¥è©¢ï¼è«åè ð CRUD æä½æ¸¬è©¦å®æ´ç¯ä¾
å°æ¡çµæ§å»ºè°
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" />
æä½³å¯¦è¸
æè©²åç â
- ç¨ç«æ¸¬è©¦å°æ¡ï¼æ´åæ¸¬è©¦å°æ¡æèå®å 測試åé¢
- æ¸¬è©¦è³æéé¢ï¼æ¯å測試æ¡ä¾æç¨ç«çè³ææºå忏 ç
- 使ç¨åºåºé¡å¥ï¼å ±ç¨çè¨å®åè¼å©æ¹æ³æ¾å¨åºåºé¡å¥
- æç¢ºçå½åï¼ä½¿ç¨ä¸æ®µå¼å½åæ³ï¼æ¹æ³_æ å¢_é æï¼
- é©ç¶ç測試ç¯åï¼å°æ³¨æ¼æ´åé»ï¼ä¸è¦é度測試
æè©²é¿å ç â
- æ··åæ¸¬è©¦é¡åï¼ä¸è¦å°å®å æ¸¬è©¦åæ´åæ¸¬è©¦æ¾å¨åä¸å°æ¡
- 測試ç¸ä¾æ§ï¼æ¯å測試æè©²ç¨ç«ï¼ä¸ä¾è³´å ¶ä»æ¸¬è©¦çå·è¡é åº
- é度模æ¬ï¼æ´å測試æè©²ç¡é使ç¨ç實çå ä»¶
- å¿½ç¥æ¸ çï¼æ¸¬è©¦å®æå¾è¦æ¸ çæ¸¬è©¦è³æ
- ç¡¬ç·¨ç¢¼è³æï¼ä½¿ç¨å·¥å» æ¹æ³æ Builder 模å¼å»ºç«æ¸¬è©¦è³æ
ç¸éæè½
unit-test-fundamentals– å®å 測試åºç¤nsubstitute-mocking– ä½¿ç¨ NSubstitute é²è¡æ¨¡æ¬awesome-assertions-guide– AwesomeAssertions æµæ¢æ·è¨testcontainers-database– ä½¿ç¨ Testcontainers é²è¡å®¹å¨åè³æåº«æ¸¬è©¦
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
- Day 19 – æ´å測試å
¥éï¼åºç¤æ¶æ§èæç¨å ´æ¯
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10376335
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day19