Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
631
colaflow-api/tests/README.md
Normal file
631
colaflow-api/tests/README.md
Normal file
@@ -0,0 +1,631 @@
|
||||
# ColaFlow Testing Guide
|
||||
|
||||
This document explains the testing strategy, setup, and best practices for ColaFlow project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Testing Philosophy](#testing-philosophy)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [CI/CD Integration](#cicd-integration)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Testing Philosophy
|
||||
|
||||
ColaFlow follows the **Test Pyramid** approach:
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E Tests (5%)
|
||||
/ \ - Critical user flows
|
||||
/------\
|
||||
/ \ Integration Tests (15%)
|
||||
/ \ - API endpoints
|
||||
/ \ - Database operations
|
||||
/--------------\
|
||||
/ \ Unit Tests (80%)
|
||||
------------------ - Domain logic
|
||||
- Application services
|
||||
- Business rules
|
||||
```
|
||||
|
||||
### Quality Standards
|
||||
|
||||
- **Minimum Code Coverage**: 80%
|
||||
- **Target Code Coverage**: 90%+
|
||||
- **Critical Path Coverage**: 100%
|
||||
- **All tests must**:
|
||||
- Be repeatable and deterministic
|
||||
- Run independently (no order dependency)
|
||||
- Clean up after themselves
|
||||
- Have clear assertions and error messages
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── ColaFlow.Domain.Tests/ # Domain unit tests
|
||||
│ ├── Aggregates/
|
||||
│ │ ├── ProjectTests.cs
|
||||
│ │ ├── EpicTests.cs
|
||||
│ │ └── TaskTests.cs
|
||||
│ ├── ValueObjects/
|
||||
│ │ ├── ProjectIdTests.cs
|
||||
│ │ └── TaskPriorityTests.cs
|
||||
│ └── DomainEvents/
|
||||
│ └── EventHandlerTests.cs
|
||||
│
|
||||
├── ColaFlow.Application.Tests/ # Application layer tests
|
||||
│ ├── Commands/
|
||||
│ │ ├── CreateProjectCommandTests.cs
|
||||
│ │ └── UpdateProjectCommandTests.cs
|
||||
│ ├── Queries/
|
||||
│ │ ├── GetProjectByIdQueryTests.cs
|
||||
│ │ └── GetKanbanBoardQueryTests.cs
|
||||
│ └── Behaviors/
|
||||
│ ├── ValidationBehaviorTests.cs
|
||||
│ └── TransactionBehaviorTests.cs
|
||||
│
|
||||
├── ColaFlow.IntegrationTests/ # Integration tests
|
||||
│ ├── API/
|
||||
│ │ ├── ProjectsApiTests.cs
|
||||
│ │ ├── TasksApiTests.cs
|
||||
│ │ └── WorkflowsApiTests.cs
|
||||
│ ├── Infrastructure/
|
||||
│ │ ├── IntegrationTestBase.cs
|
||||
│ │ └── WebApplicationFactoryBase.cs
|
||||
│ └── Database/
|
||||
│ ├── RepositoryTests.cs
|
||||
│ └── MigrationTests.cs
|
||||
│
|
||||
├── ExampleDomainTest.cs # Template domain test
|
||||
├── ExampleIntegrationTest.cs # Template integration test
|
||||
├── IntegrationTestBase.cs # Base class for integration tests
|
||||
├── WebApplicationFactoryBase.cs # WebApplicationFactory setup
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **.NET 9 SDK** (includes testing tools)
|
||||
- **Docker Desktop** (for Testcontainers)
|
||||
- **IDE**: Visual Studio 2022, JetBrains Rider, or VS Code
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Ensure Docker Desktop is running**:
|
||||
```bash
|
||||
docker --version
|
||||
docker ps
|
||||
```
|
||||
|
||||
2. **Restore NuGet packages** (if not already done):
|
||||
```bash
|
||||
cd tests
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
3. **Verify test projects build**:
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### Creating Test Projects
|
||||
|
||||
If test projects don't exist yet, use the provided templates:
|
||||
|
||||
```bash
|
||||
# Domain Tests
|
||||
cd tests
|
||||
dotnet new xunit -n ColaFlow.Domain.Tests
|
||||
cp ColaFlow.Domain.Tests.csproj.template ColaFlow.Domain.Tests/ColaFlow.Domain.Tests.csproj
|
||||
|
||||
# Application Tests
|
||||
dotnet new xunit -n ColaFlow.Application.Tests
|
||||
cp ColaFlow.Application.Tests.csproj.template ColaFlow.Application.Tests/ColaFlow.Application.Tests.csproj
|
||||
|
||||
# Integration Tests
|
||||
dotnet new xunit -n ColaFlow.IntegrationTests
|
||||
cp ColaFlow.IntegrationTests.csproj.template ColaFlow.IntegrationTests/ColaFlow.IntegrationTests.csproj
|
||||
|
||||
# Restore packages
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All Tests
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
dotnet test
|
||||
|
||||
# From tests directory
|
||||
cd tests
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### Run Specific Test Project
|
||||
|
||||
```bash
|
||||
# Domain tests only
|
||||
dotnet test ColaFlow.Domain.Tests/ColaFlow.Domain.Tests.csproj
|
||||
|
||||
# Integration tests only
|
||||
dotnet test ColaFlow.IntegrationTests/ColaFlow.IntegrationTests.csproj
|
||||
```
|
||||
|
||||
### Run Specific Test Class
|
||||
|
||||
```bash
|
||||
dotnet test --filter FullyQualifiedName~ProjectTests
|
||||
```
|
||||
|
||||
### Run Specific Test Method
|
||||
|
||||
```bash
|
||||
dotnet test --filter FullyQualifiedName~ProjectTests.Create_ValidData_ShouldCreateProject
|
||||
```
|
||||
|
||||
### Run Tests by Category
|
||||
|
||||
```bash
|
||||
# Run only unit tests
|
||||
dotnet test --filter Category=Unit
|
||||
|
||||
# Run only integration tests
|
||||
dotnet test --filter Category=Integration
|
||||
|
||||
# Exclude slow tests
|
||||
dotnet test --filter Category!=Slow
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```bash
|
||||
# Run tests in parallel (default)
|
||||
dotnet test --parallel
|
||||
|
||||
# Run tests sequentially (for debugging)
|
||||
dotnet test --parallel none
|
||||
```
|
||||
|
||||
### Verbose Output
|
||||
|
||||
```bash
|
||||
# Detailed output
|
||||
dotnet test --logger "console;verbosity=detailed"
|
||||
|
||||
# Minimal output
|
||||
dotnet test --logger "console;verbosity=minimal"
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Unit Tests (Domain Layer)
|
||||
|
||||
**Example**: Testing Project Aggregate
|
||||
|
||||
```csharp
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Domain.Tests.Aggregates;
|
||||
|
||||
public class ProjectTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ValidData_ShouldCreateProject()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Test Project";
|
||||
var description = "Test Description";
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
var project = Project.Create(name, description, key, ownerId);
|
||||
|
||||
// Assert
|
||||
project.Should().NotBeNull();
|
||||
project.Name.Should().Be(name);
|
||||
project.Key.Value.Should().Be(key);
|
||||
project.Status.Should().Be(ProjectStatus.Active);
|
||||
project.DomainEvents.Should().ContainSingle(e => e is ProjectCreatedEvent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
[InlineData(" ")]
|
||||
public void Create_InvalidName_ShouldThrowException(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var key = "TEST";
|
||||
var ownerId = UserId.Create(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
Action act = () => Project.Create(invalidName, "", key, ownerId);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<DomainException>()
|
||||
.WithMessage("Project name cannot be empty");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Application Layer Tests (CQRS)
|
||||
|
||||
**Example**: Testing Command Handler
|
||||
|
||||
```csharp
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.Application.Tests.Commands;
|
||||
|
||||
public class CreateProjectCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||
private readonly Mock<ICurrentUserService> _currentUserServiceMock;
|
||||
private readonly CreateProjectCommandHandler _handler;
|
||||
|
||||
public CreateProjectCommandHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||
_currentUserServiceMock = new Mock<ICurrentUserService>();
|
||||
_handler = new CreateProjectCommandHandler(
|
||||
_projectRepositoryMock.Object,
|
||||
_unitOfWorkMock.Object,
|
||||
_currentUserServiceMock.Object,
|
||||
Mock.Of<IMapper>(),
|
||||
Mock.Of<ILogger<CreateProjectCommandHandler>>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidCommand_CreatesProject()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateProjectCommand
|
||||
{
|
||||
Name = "Test Project",
|
||||
Description = "Description",
|
||||
Key = "TEST"
|
||||
};
|
||||
|
||||
_currentUserServiceMock.Setup(x => x.UserId).Returns(Guid.NewGuid());
|
||||
_projectRepositoryMock.Setup(x => x.GetByKeyAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync((Project?)null);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, default);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_projectRepositoryMock.Verify(x => x.AddAsync(It.IsAny<Project>(), default), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.CommitAsync(default), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DuplicateKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateProjectCommand { Name = "Test", Key = "TEST" };
|
||||
var existingProject = Project.Create("Existing", "", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
|
||||
_projectRepositoryMock.Setup(x => x.GetByKeyAsync("TEST", default))
|
||||
.ReturnsAsync(existingProject);
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _handler.Handle(command, default);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<DomainException>()
|
||||
.WithMessage("*already exists*");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests (API)
|
||||
|
||||
**Example**: Testing API Endpoint
|
||||
|
||||
```csharp
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ColaFlow.IntegrationTests.API;
|
||||
|
||||
[Collection("IntegrationTests")]
|
||||
public class ProjectsApiTests : IClassFixture<ColaFlowWebApplicationFactory<Program, ColaFlowDbContext>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> _factory;
|
||||
|
||||
public ProjectsApiTests(ColaFlowWebApplicationFactory<Program, ColaFlowDbContext> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateProject_ValidData_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var createRequest = new CreateProjectDto
|
||||
{
|
||||
Name = "Test Project",
|
||||
Description = "Test Description",
|
||||
Key = "TEST"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/projects", createRequest);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var project = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
project.Should().NotBeNull();
|
||||
project!.Name.Should().Be("Test Project");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetProject_ExistingId_ReturnsProject()
|
||||
{
|
||||
// Arrange - Seed data
|
||||
using var scope = _factory.CreateScope();
|
||||
var dbContext = _factory.GetDbContext(scope);
|
||||
|
||||
var project = Project.Create("Test", "Description", "TEST", UserId.Create(Guid.NewGuid()));
|
||||
await dbContext.Projects.AddAsync(project);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/api/v1/projects/{project.Id.Value}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var returnedProject = await response.Content.ReadFromJsonAsync<ProjectDto>();
|
||||
returnedProject.Should().NotBeNull();
|
||||
returnedProject!.Name.Should().Be("Test");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Generate Coverage Report
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
|
||||
# Generate HTML report (requires ReportGenerator)
|
||||
dotnet tool install -g dotnet-reportgenerator-globaltool
|
||||
reportgenerator -reports:coverage.opencover.xml -targetdir:coveragereport -reporttypes:Html
|
||||
|
||||
# Open report
|
||||
start coveragereport/index.html # Windows
|
||||
open coveragereport/index.html # Mac
|
||||
```
|
||||
|
||||
### Coverage Thresholds
|
||||
|
||||
Configure in test project `.csproj`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<CoverletOutputFormat>opencover</CoverletOutputFormat>
|
||||
<Threshold>80</Threshold>
|
||||
<ThresholdType>line,branch,method</ThresholdType>
|
||||
<ThresholdStat>total</ThresholdStat>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Exclude from Coverage
|
||||
|
||||
```csharp
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class Startup { }
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Tests run automatically on every push and pull request. See `.github/workflows/test.yml`.
|
||||
|
||||
### Local CI Simulation
|
||||
|
||||
```bash
|
||||
# Simulate CI environment
|
||||
dotnet clean
|
||||
dotnet restore
|
||||
dotnet build --no-restore
|
||||
dotnet test --no-build --verbosity normal
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### General Principles
|
||||
|
||||
1. **Arrange-Act-Assert (AAA) Pattern**
|
||||
```csharp
|
||||
[Fact]
|
||||
public void TestMethod()
|
||||
{
|
||||
// Arrange - Setup test data and dependencies
|
||||
var input = "test";
|
||||
|
||||
// Act - Execute the method under test
|
||||
var result = MethodUnderTest(input);
|
||||
|
||||
// Assert - Verify the result
|
||||
result.Should().Be("expected");
|
||||
}
|
||||
```
|
||||
|
||||
2. **One Assertion Per Test** (when practical)
|
||||
- Makes failures easier to diagnose
|
||||
- Exception: Related assertions (e.g., checking object properties)
|
||||
|
||||
3. **Test Naming Convention**
|
||||
```
|
||||
MethodName_StateUnderTest_ExpectedBehavior
|
||||
```
|
||||
Examples:
|
||||
- `Create_ValidData_ShouldCreateProject`
|
||||
- `Create_EmptyName_ShouldThrowException`
|
||||
- `GetById_NonExistentId_ReturnsNotFound`
|
||||
|
||||
4. **Test Independence**
|
||||
- Tests should not depend on execution order
|
||||
- Each test should clean up after itself
|
||||
- Use test fixtures for shared setup
|
||||
|
||||
5. **Avoid Test Logic**
|
||||
- No loops, conditionals, or complex logic in tests
|
||||
- Tests should be simple and readable
|
||||
|
||||
### Domain Tests
|
||||
|
||||
- Test business rules and invariants
|
||||
- Test domain events are raised
|
||||
- Test value object equality
|
||||
- No mocking (pure unit tests)
|
||||
|
||||
### Application Tests
|
||||
|
||||
- Mock infrastructure dependencies (repositories, services)
|
||||
- Test command/query handlers
|
||||
- Test validation logic
|
||||
- Test MediatR pipeline behaviors
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Use Testcontainers for real databases
|
||||
- Test complete request/response flows
|
||||
- Test database operations
|
||||
- Test authentication/authorization
|
||||
- Clean database between tests
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Keep unit tests fast (< 100ms each)
|
||||
- Integration tests can be slower (< 5s each)
|
||||
- Use `[Fact(Skip = "Reason")]` for slow tests during development
|
||||
- Run slow tests in CI only
|
||||
|
||||
### Data Builders
|
||||
|
||||
Use builder pattern for complex test data:
|
||||
|
||||
```csharp
|
||||
public class ProjectBuilder
|
||||
{
|
||||
private string _name = "Test Project";
|
||||
private string _key = "TEST";
|
||||
|
||||
public ProjectBuilder WithName(string name)
|
||||
{
|
||||
_name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProjectBuilder WithKey(string key)
|
||||
{
|
||||
_key = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Project Build()
|
||||
{
|
||||
return Project.Create(_name, "Description", _key, UserId.Create(Guid.NewGuid()));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
var project = new ProjectBuilder()
|
||||
.WithName("Custom Project")
|
||||
.WithKey("CUSTOM")
|
||||
.Build();
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker Not Running
|
||||
|
||||
**Error**: `Unable to connect to Docker daemon`
|
||||
|
||||
**Solution**: Start Docker Desktop and ensure it's fully initialized.
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
**Error**: `Address already in use`
|
||||
|
||||
**Solution**: Stop conflicting services or use different ports in `docker-compose.yml`.
|
||||
|
||||
### Test Database Not Clean
|
||||
|
||||
**Issue**: Tests fail due to leftover data
|
||||
|
||||
**Solution**: Use `CleanDatabaseAsync()` in test setup or use Testcontainers (auto-cleanup).
|
||||
|
||||
### Slow Tests
|
||||
|
||||
**Issue**: Integration tests taking too long
|
||||
|
||||
**Solutions**:
|
||||
- Use Testcontainers' shared fixture (reuse containers)
|
||||
- Optimize database queries
|
||||
- Use in-memory database for simple tests
|
||||
- Run integration tests selectively
|
||||
|
||||
### Flaky Tests
|
||||
|
||||
**Issue**: Tests pass/fail intermittently
|
||||
|
||||
**Common causes**:
|
||||
- Race conditions (async/await issues)
|
||||
- Time-dependent assertions
|
||||
- External service dependencies
|
||||
- Database transaction issues
|
||||
|
||||
**Solutions**:
|
||||
- Use proper async/await
|
||||
- Mock time-dependent code
|
||||
- Use Testcontainers for isolation
|
||||
- Ensure proper transaction handling
|
||||
|
||||
## Resources
|
||||
|
||||
- [xUnit Documentation](https://xunit.net/)
|
||||
- [FluentAssertions Documentation](https://fluentassertions.com/)
|
||||
- [Testcontainers Documentation](https://dotnet.testcontainers.org/)
|
||||
- [Architecture Design](../docs/M1-Architecture-Design.md)
|
||||
- [Docker Setup](../DOCKER-README.md)
|
||||
|
||||
## Support
|
||||
|
||||
For testing issues:
|
||||
1. Check this README
|
||||
2. Review test examples in this directory
|
||||
3. Consult architecture documentation
|
||||
4. Ask team for help
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-02
|
||||
**Maintained By**: QA Team
|
||||
**Quality Standard**: 80%+ Coverage, All Tests Green
|
||||
Reference in New Issue
Block a user