dotnet-testing-advanced-aspire-testing
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-advanced-aspire-testing
Agent 安装分布
Skill 文档
.NET Aspire Testing æ´åæ¸¬è©¦æ¡æ¶
é©ç¨æ å¢
ç¶è¢«è¦æ±å·è¡ä»¥ä¸ä»»åæï¼è«ä½¿ç¨æ¤æè½ï¼
- çº .NET Aspire 忣弿ç¨å»ºç«æ´å測試
- å¾ Testcontainers é·ç§»å° .NET Aspire Testing
- è¨å® AppHost å°æ¡é²è¡æ¸¬è©¦
- ä½¿ç¨ DistributedApplicationTestingBuilder å»ºç«æ¸¬è©¦ç°å¢
- éè¦æ¸¬è©¦å¤åæåéçäºåï¼è³æåº«ãå¿«åãAPI çï¼
- 建ç«é²åç .NET æç¨çæ´åæ¸¬è©¦æ¶æ§
åç½®éæ±
- .NET 8 SDK ææ´é«çæ¬
- Docker Desktopï¼WSL 2 æ Hyper-Vï¼
- AppHost å°æ¡ï¼.NET Aspire æç¨ç·¨æï¼
æ ¸å¿æ¦å¿µ
.NET Aspire Testing å®ä½
.NET Aspire Testing æ¯å°é弿´åæ¸¬è©¦æ¡æ¶ï¼å°çºåæ£å¼æç¨è¨è¨ï¼
- 卿¸¬è©¦ä¸éç¾èæ£å¼ç°å¢ç¸åçæåæ¶æ§
- 使ç¨ç實容å¨è鿍¡æ¬æå
- èªå管ç容å¨çå½é±æ
AppHost å°æ¡çå¿ è¦æ§
ä½¿ç¨ .NET Aspire Testing å¿ é å»ºç« AppHost å°æ¡ï¼
- å®ç¾©å®æ´çæç¨æ¶æ§å容å¨ç·¨æ
- 測試éç¨ AppHost é 置建ç«ç°å¢
- æ²æ AppHost å°±ç¡æ³ä½¿ç¨ Aspire Testing
è Testcontainers çå·®ç°
| ç¹æ§ | .NET Aspire Testing | Testcontainers |
|---|---|---|
| è¨è¨ç®æ¨ | é²åçåæ£å¼æç¨ | éç¨å®¹å¨æ¸¬è©¦ |
| é ç½®æ¹å¼ | AppHost 宣åå¼å®ç¾© | ç¨å¼ç¢¼æåé ç½® |
| æåç·¨æ | èªåèç | æå管ç |
| å¸ç¿æ²ç· | è¼é« | ä¸ç |
| é©ç¨å ´æ¯ | å·²ç¨ Aspire çå°æ¡ | å³çµ± Web API |
å°æ¡çµæ§
MyApp/
âââ src/
â âââ MyApp.Api/ # WebApi 層
â âââ MyApp.Application/ # æç¨æå層
â âââ MyApp.Domain/ # é 忍¡å
â âââ MyApp.Infrastructure/ # åºç¤è¨æ½å±¤
âââ MyApp.AppHost/ # Aspire ç·¨æå°æ¡ â
â âââ MyApp.AppHost.csproj
â âââ Program.cs
âââ tests/
âââ MyApp.Tests.Integration/ # Aspire Testing æ´å測試
âââ MyApp.Tests.Integration.csproj
âââ Infrastructure/
â âââ AspireAppFixture.cs
â âââ IntegrationTestCollection.cs
â âââ IntegrationTestBase.cs
â âââ DatabaseManager.cs
âââ Controllers/
âââ MyControllerTests.cs
å¿ è¦å¥ä»¶
AppHost å°æ¡
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.1.0" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.1.0" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\MyApp.Api\MyApp.Api.csproj" />
</ItemGroup>
</Project>
æ¸¬è©¦å°æ¡
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.1.0" />
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Respawn" Version="6.2.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\MyApp.AppHost\MyApp.AppHost.csproj" />
</ItemGroup>
</Project>
容å¨çå½é±æç®¡ç
ä½¿ç¨ ContainerLifetime.Session ç¢ºä¿æ¸¬è©¦è³æºèªåæ¸
çï¼
var postgres = builder.AddPostgres("postgres")
.WithLifetime(ContainerLifetime.Session);
var redis = builder.AddRedis("redis")
.WithLifetime(ContainerLifetime.Session);
- Sessionï¼æ¸¬è©¦æè©±çµæå¾èªåæ¸ çï¼æ¨è¦ï¼
- Persistentï¼å®¹å¨æçºéè¡ï¼éæåæ¸ ç
çå¾ æåå°±ç·
容å¨ååèæåå°±ç·æ¯å ©åéæ®µï¼éè¦çå¾ æ©å¶ï¼
private async Task WaitForPostgreSqlReadyAsync()
{
const int maxRetries = 30;
const int delayMs = 1000;
for (int i = 0; i < maxRetries; i++)
{
try
{
var connectionString = await GetConnectionStringAsync();
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
return;
}
catch (Exception ex) when (i < maxRetries - 1)
{
await Task.Delay(delayMs);
}
}
throw new InvalidOperationException("PostgreSQL æåæªè½å°±ç·");
}
è³æåº«åå§å
Aspire åå容å¨ä½ä¸èªå建ç«è³æåº«ï¼
private async Task EnsureDatabaseExistsAsync(string connectionString)
{
var builder = new NpgsqlConnectionStringBuilder(connectionString);
var databaseName = builder.Database;
builder.Database = "postgres"; // é£å°é è¨è³æåº«
await using var connection = new NpgsqlConnection(builder.ToString());
await connection.OpenAsync();
var checkDbQuery = $"SELECT 1 FROM pg_database WHERE datname = '{databaseName}'";
var dbExists = await new NpgsqlCommand(checkDbQuery, connection).ExecuteScalarAsync();
if (dbExists == null)
{
await new NpgsqlCommand($"CREATE DATABASE \"{databaseName}\"", connection)
.ExecuteNonQueryAsync();
}
}
Respawn é ç½®
ä½¿ç¨ PostgreSQL æå¿ é æå®é©é å¨ï¼
_respawner = await Respawner.CreateAsync(connection, new RespawnerOptions
{
TablesToIgnore = new Table[] { "__EFMigrationsHistory" },
SchemasToInclude = new[] { "public" },
DbAdapter = DbAdapter.Postgres // ééµï¼
});
Collection Fixture æä½³å¯¦è¸
é¿å æ¯å測試é¡å¥éè¤åå容å¨ï¼
[CollectionDefinition("Integration Tests")]
public class IntegrationTestCollection : ICollectionFixture<AspireAppFixture>
{
}
[Collection("Integration Tests")]
public class MyControllerTests : IntegrationTestBase
{
public MyControllerTests(AspireAppFixture fixture) : base(fixture) { }
}
æé坿¸¬è©¦æ§
ä½¿ç¨ TimeProvider æ½è±¡åæéä¾è³´ï¼
// æå實ä½
public class ProductService
{
private readonly TimeProvider _timeProvider;
public ProductService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public async Task<Product> CreateAsync(ProductCreateRequest request)
{
var now = _timeProvider.GetUtcNow();
var product = new Product
{
CreatedAt = now,
UpdatedAt = now
};
// ...
}
}
// DI 註å
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
é¸æå»ºè°
鏿 .NET Aspire Testing
- å°æ¡å·²ä½¿ç¨ .NET Aspire
- éè¦æ¸¬è©¦å¤æåäºå
- éè¦çµ±ä¸çéç¼æ¸¬è©¦é«é©
- é²åçæç¨æ¶æ§
鏿 Testcontainers
- å³çµ± .NET å°æ¡
- éè¦ç²¾ç´°ç容卿§å¶
- èé .NET æåæ´å
- åéå° Aspire ä¸çæ
常è¦åé¡
端é»é ç½®è¡çª
ä¸è¦æåé ç½®å·²ç± Aspire èªåèçç端é»ï¼
// â é¯èª¤ï¼æé æè¡çª
builder.AddProject<Projects.MyApp_Api>("my-api")
.WithHttpEndpoint(port: 8080, name: "http");
// â
æ£ç¢ºï¼è® Aspire èªåèç
builder.AddProject<Projects.MyApp_Api>("my-api")
.WithReference(postgresDb)
.WithReference(redis);
Dapper æ¬ä½æ å°
PostgreSQL snake_case è C# PascalCase çæ å°ï¼
// å¨ Program.cs åå§å
DapperTypeMapping.Initialize();
// æä½¿ç¨ SQL å¥å
const string sql = @"
SELECT id, name, price,
created_at AS CreatedAt,
updated_at AS UpdatedAt
FROM products";
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
-
Day 24 – .NET Aspire Testing å ¥éåºç¤ä»ç´¹
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10377071
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day24
-
Day 25 – .NET Aspire æ´å測試實æ°ï¼å¾ Testcontainers å° .NET Aspire Testing
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10377197
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day25
宿¹æä»¶
ç¨å¼ç¢¼ç¯ä¾
è«åèåç®éä¸çç¯ä¾æªæ¡ï¼
templates/apphost-program.cs– AppHost ç·¨æå®ç¾©templates/aspire-app-fixture.cs– 測試åºç¤è¨æ½templates/integration-test-collection.cs– Collection Fixture è¨å®templates/integration-test-base.cs– 測試åºåºé¡å¥templates/database-manager.cs– è³æåº«ç®¡çå¡templates/controller-tests.cs– æ§å¶å¨æ¸¬è©¦ç¯ä¾templates/test-project.csproj– æ¸¬è©¦å°æ¡è¨å®templates/apphost-project.csproj– AppHost å°æ¡è¨å®