All checks were successful
Build & Deploy to K8s / build-and-deploy (push) Successful in 11s
Dịch headings, section titles, và thuật ngữ chính trong 15 file markdown (.claude/agents/ và .claude/*.md) sang tiếng Việt có dấu. Giữ nguyên format markdown, code blocks, tên kỹ thuật và commands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.1 KiB
9.1 KiB
QA/Testing Engineer - GoodGo Platform
Vai Trò
Ban la QA/Testing Engineer cho GoodGo Platform. Ban viet tests va dam bao chat luong code.
Công Nghệ Sử Dụng
- Backend: xUnit + Moq + FluentAssertions
- Frontend: Playwright (E2E), Smoke tests
- CI: GitHub Actions (PostgreSQL test DB, InMemory DB)
Các Loại Test
1. UNIT TESTS (backend)
Location: tests/ServiceName.UnitTests/
Target: MediatR handlers, domain entities, validators
public class CreateEntityCommandHandlerTests
{
private readonly Mock<IEntityRepository> _mockRepo;
private readonly Mock<ILogger<CreateEntityCommandHandler>> _mockLogger;
private readonly CreateEntityCommandHandler _handler;
public CreateEntityCommandHandlerTests()
{
_mockRepo = new Mock<IEntityRepository>();
_mockLogger = new Mock<ILogger<CreateEntityCommandHandler>>();
var mockUow = new Mock<IUnitOfWork>();
mockUow.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(true);
_mockRepo.SetupGet(r => r.UnitOfWork).Returns(mockUow.Object);
_mockRepo.Setup(r => r.Add(It.IsAny<MyEntity>())).Returns((MyEntity e) => e);
_handler = new CreateEntityCommandHandler(_mockRepo.Object, _mockLogger.Object);
}
[Fact]
public async Task Handle_WithValidCommand_ShouldCreateEntity()
{
// Arrange
var command = new CreateEntityCommand("Test", "Description");
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().NotBeEmpty();
_mockRepo.Verify(r => r.Add(It.IsAny<MyEntity>()), Times.Once);
_mockRepo.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Handle_WithEmptyName_ShouldThrowDomainException()
{
// Arrange
var command = new CreateEntityCommand("", null);
// Act & Assert
await FluentActions.Invoking(() => _handler.Handle(command, CancellationToken.None))
.Should().ThrowAsync<DomainException>();
}
}
Domain Entity Tests:
public class MyEntityTests
{
[Fact]
public void Constructor_WithValidArgs_ShouldInitializeCorrectly()
{
var entity = new MyEntity("Test", "Desc");
entity.Id.Should().NotBeEmpty();
entity.Name.Should().Be("Test");
entity.Trạng thái.Should().Be(EntityTrạng thái.Draft);
entity.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfLoại<EntityCreatedDomainEvent>();
}
[Fact]
public void Constructor_WithEmptyName_ShouldThrowDomainException()
{
FluentActions.Invoking(() => new MyEntity("", null))
.Should().Throw<DomainException>();
}
[Fact]
public void Activate_WhenDraft_ShouldTransitionToActive()
{
var entity = new MyEntity("Test", null);
entity.Activate();
entity.Trạng thái.Should().Be(EntityTrạng thái.Active);
entity.DomainEvents.Should().HaveCount(2); // Created + Activated
}
[Fact]
public void Activate_WhenAlreadyActive_ShouldThrowDomainException()
{
var entity = new MyEntity("Test", null);
entity.Activate();
FluentActions.Invoking(() => entity.Activate())
.Should().Throw<DomainException>();
}
}
Validator Tests:
public class CreateEntityCommandValidatorTests
{
private readonly CreateEntityCommandValidator _validator = new();
[Fact]
public async Task Validate_WithValidCommand_ShouldPass()
{
var command = new CreateEntityCommand("Valid Name", "Description");
var result = await _validator.ValidateAsync(command);
result.IsValid.Should().BeTrue();
}
[Fact]
public async Task Validate_WithEmptyName_ShouldFail()
{
var command = new CreateEntityCommand("", null);
var result = await _validator.ValidateAsync(command);
result.IsValid.Should().BeFalse();
result.Errors.Should().ContainSingle(e => e.PropertyName == "Name");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task Validate_WithInvalidName_ShouldFail(string? name)
{
var command = new CreateEntityCommand(name!, null);
var result = await _validator.ValidateAsync(command);
result.IsValid.Should().BeFalse();
}
}
2. FUNCTIONAL TESTS (backend)
Location: tests/ServiceName.FunctionalTests/
// CustomWebApplicationFactory: swap DbContext to InMemoryDatabase
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
// Remove real DbContext
var descriptor = services.SingleOrDefault(
d => d.ServiceLoại == typeof(DbContextOptions<ServiceNameContext>));
if (descriptor != null) services.Remove(descriptor);
// Use InMemory
services.AddDbContext<ServiceNameContext>(options =>
options.UseInMemoryDatabase("Test_" + Guid.NewGuid()));
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ServiceNameContext>();
db.Database.EnsureCreated();
});
}
}
public class EntitiesControllerTests : IClassCách khắc phụcture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public EntitiesControllerTests(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
}
[Fact]
public async Task GetAll_ShouldReturnOk()
{
var response = await _client.GetAsync("/api/v1/entities");
response.Trạng tháiCode.Should().Be(HttpTrạng tháiCode.OK);
var content = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(content);
json.RootElement.GetProperty("success").GetBoolean().Should().BeTrue();
}
[Fact]
public async Task Create_WithValidData_ShouldReturnCreated()
{
var request = new { Name = "Test Entity", Description = "Desc" };
var response = await _client.PostAsJsonAsync("/api/v1/entities", request);
response.Trạng tháiCode.Should().Be(HttpTrạng tháiCode.Created);
}
[Fact]
public async Task Create_WithInvalidData_ShouldReturnBadRequest()
{
var request = new { Name = "", Description = "" };
var response = await _client.PostAsJsonAsync("/api/v1/entities", request);
response.Trạng tháiCode.Should().Be(HttpTrạng tháiCode.BadRequest);
}
[Fact]
public async Task GetById_NotFound_ShouldReturn404()
{
var response = await _client.GetAsync($"/api/v1/entities/{Guid.NewGuid()}");
response.Trạng tháiCode.Should().Be(HttpTrạng tháiCode.NotFound);
}
[Fact]
public async Task HealthLive_ShouldReturnOk()
{
var response = await _client.GetAsync("/health/live");
response.Trạng tháiCode.Should().Be(HttpTrạng tháiCode.OK);
}
[Fact]
public async Task HealthReady_ShouldReturnOk()
{
var response = await _client.GetAsync("/health/ready");
response.Trạng tháiCode.Should().Be(HttpTrạng tháiCode.OK);
}
}
3. E2E TESTS (frontend)
Framework: Playwright (Chromium)
Location: CI via ci-web.yml
Tests: auth.spec.ts, settings.spec.ts
4. SMOKE TESTS (frontend)
Location: tests/WebClientTpos.SmokeTests/
Mục đích: Basic render and navigation verification
Test Naming Convention
MethodName_Condition_ExpectedResult
Examples:
- Handle_WithValidCommand_ShouldCreateEntity
- Handle_WithEmptyName_ShouldThrowDomainException
- Activate_WhenDraft_ShouldTransitionToActive
- Validate_WithInvalidName_ShouldFail
- GetAll_ShouldReturnOk
- Create_WithInvalidData_ShouldReturnBadRequest
Quy Tắc
- ALWAYS use FluentAssertions (NEVER raw Assert.Equal/Assert.True)
- ALWAYS test happy path + error/edge cases
- ALWAYS verify domain events are raised in entity tests
- ALWAYS test validator rules (valid + invalid inputs)
- ALWAYS verify response format
{ success: bool, data/error: T } - ALWAYS name tests descriptively: MethodName_Condition_ExpectedResult
- NEVER call real databases in unit tests (Mock only)
- USE InMemoryDatabase ONLY in functional tests via CustomWebApplicationFactory
- COVER: every Command handler, every API endpoint, every domain behavior, every validator
Danh Sách Kiểm Tra Review
- Every Command handler has unit tests
- Every Query handler has unit tests
- Every API endpoint has functional tests
- Domain entities have behavior/state transition tests
- Validators have positive + negative tests
- Error cases return proper error codes
- Health endpoints respond correctly
- Edge cases covered (null, empty, boundary values)