coder-csharp-testing
npx skills add https://github.com/ozerohax/assistagents --skill coder-csharp-testing
Agent 安装分布
Skill 文档
<skill_overview> Write effective, maintainable tests following industry best practices Writing unit tests Setting up integration tests Mocking dependencies Testing EF Core repositories Testing ASP.NET Core APIs Microsoft Testing Documentation xUnit Official Documentation </skill_overview> <framework_choice> Use xUnit for new projects Modern design by original NUnit authors New instance per test (better isolation) No class-level attributes needed Community standard for .NET <test_attr>[Fact], [Theory]</test_attr> Constructor + IDisposable New instance per test <test_attr>[Test], [TestCase]</test_attr> [SetUp], [TearDown] Same instance per class <test_attr>[TestMethod]</test_attr> [TestInitialize] Same instance per class </framework_choice> <unit_testing> Structure every test with AAA pattern [Fact] public void Add_TwoPositiveNumbers_ReturnsSum() { // Arrange var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
} <naming_convention> [Method][Scenario][ExpectedResult] Add_TwoPositiveNumbers_ReturnsSum GetUser_NonExistentId_ReturnsNull CreateOrder_EmptyCart_ThrowsException Test1, TestAdd, CalculatorTest </naming_convention> One logical assertion per test Multiple related assertions for same concept are OK [Fact] public void CreateUser_ValidInput_ReturnsUserWithCorrectProperties() { var user = _service.CreateUser(“john@example.com“, “John”);
// Multiple assertions for one logical concept (user created correctly)
Assert.NotNull(user);
Assert.Equal("john@example.com", user.Email);
Assert.Equal("John", user.Name);
Assert.True(user.Id > 0);
} <test_isolation> No shared mutable state between tests Don’t depend on test execution order Each test must be independently runnable Clean up resources in Dispose public class OrderServiceTests : IDisposable { private readonly Mock<IOrderRepository> _mockRepo; private readonly OrderService _service;
public OrderServiceTests()
{
// Fresh instance for each test
_mockRepo = new Mock<IOrderRepository>();
_service = new OrderService(_mockRepo.Object);
}
public void Dispose()
{
// Cleanup if needed
}
} </test_isolation> <private_methods> NEVER test private methods directly Tests should verify public behavior, not implementation Makes refactoring harder If private method is complex, extract to separate class Test through public API or extract to testable class </private_methods> </unit_testing> <library_choice> Moq (most popular) or NSubstitute (cleaner syntax) </library_choice> <moq_patterns> <setup_returns> var mockRepo = new Mock<IUserRepository>(); mockRepo.Setup(r => r.GetById(1)) .Returns(new User { Id = 1, Name = “John” }); var service = new UserService(mockRepo.Object); </setup_returns> <setup_async> mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>())) .ReturnsAsync(new User { Id = 1 }); </setup_async> // Verify method was called mockRepo.Verify(r => r.Save(It.IsAny<User>()), Times.Once()); // Verify with specific argument mockRepo.Verify(r => r.Save(It.Is<User>(u => u.Email == “test@example.com“))); // Verify never called mockRepo.Verify(r => r.Delete(It.IsAny<int>()), Times.Never()); It.IsAny<int>() // Any value It.Is<User>(u => u.Id > 0) // Condition It.IsIn(1, 2, 3) // One of values It.IsRegex(@”^\d+$”) // Regex match </moq_patterns> <when_to_mock> External dependencies (APIs, databases, file system) Slow operations Non-deterministic operations (DateTime, Random) <dont_mock>Value objects, DTOs, simple classes</dont_mock> <dont_mock>The class under test</dont_mock> </when_to_mock> <best_practices> Setup only what you need for the test Prefer Returns over Verify when possible Use strict mocks sparingly Don’t mock what you don’t own (wrap it first) </best_practices> <testing_efcore> NEVER mock DbContext – use real database instead LINQ queries don’t work correctly with mocks Change tracking doesn’t work Tests pass but production fails <sqlite_inmemory recommended=”true”> Use SQLite in-memory for fast, realistic tests public class RepositoryTests : IDisposable { private readonly SqliteConnection _connection; private readonly AppDbContext _context;
public RepositoryTests()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(_connection)
.Options;
_context = new AppDbContext(options);
_context.Database.EnsureCreated();
}
[Fact]
public async Task Add_ValidEntity_SavesSuccessfully()
{
var repo = new UserRepository(_context);
var user = new User { Email = "test@example.com" };
await repo.AddAsync(user);
var saved = await _context.Users.FindAsync(user.Id);
Assert.NotNull(saved);
Assert.Equal("test@example.com", saved.Email);
}
public void Dispose()
{
_context.Dispose();
_connection.Dispose();
}
} </sqlite_inmemory> <inmemory_provider> Use only for simple cases – NOT a real relational database No referential integrity No transactions Different SQL translation </inmemory_provider> Use for critical integration tests with real database public class DatabaseFixture : IAsyncLifetime { private readonly MsSqlContainer _container; public string ConnectionString => _container.GetConnectionString();
public DatabaseFixture()
{
_container = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
}
public async Task InitializeAsync() => await _container.StartAsync();
public async Task DisposeAsync() => await _container.StopAsync();
} </testing_efcore> <integration_testing> Standard for ASP.NET Core integration tests public class ApiTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client;
public ApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetUsers_ReturnsSuccessStatusCode()
{
var response = await _client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
Assert.Equal("application/json",
response.Content.Headers.ContentType?.MediaType);
}
} <custom_factory> Override services for testing public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // Replace real database with SQLite var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<AppDbContext>)); services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseSqlite("DataSource=:memory:"));
// Replace external services with fakes
services.AddScoped<IEmailService, FakeEmailService>();
});
builder.UseEnvironment("Testing");
}
} </custom_factory> <test_authentication> public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions> { protected override Task<AuthenticateResult> HandleAuthenticateAsync() { var claims = new[] { new Claim(ClaimTypes.Name, “TestUser”), new Claim(ClaimTypes.Role, “Admin”) }; var identity = new ClaimsIdentity(claims, “Test”); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, “TestScheme”);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
} </test_authentication> </integration_testing> <code_coverage> Coverage is a tool, not a goal Focus on critical paths, not 100% Test without assertions = coverage without value 80-90% 70-80% 50-70% <what_not_to_test> Auto-generated code Configuration/startup code Simple DTOs Third-party library wrappers </what_not_to_test> dotnet test –collect:”XPlat Code Coverage” </code_coverage> <theory_tests> Parameterized tests for multiple inputs [Theory] [InlineData(1, 2, 3)] [InlineData(-1, 1, 0)] [InlineData(0, 0, 0)] public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected) { var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
} [Theory] [MemberData(nameof(GetTestData))] public void Process_TestCases_ReturnsExpected(Order order, bool expected) { var result = _service.IsValid(order); Assert.Equal(expected, result); } public static IEnumerable<object[]> GetTestData() { yield return new object[] { new Order { Total = 100 }, true }; yield return new object[] { new Order { Total = 0 }, false }; } </theory_tests>