# 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() .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 _projectRepositoryMock; private readonly Mock _unitOfWorkMock; private readonly Mock _currentUserServiceMock; private readonly CreateProjectCommandHandler _handler; public CreateProjectCommandHandlerTests() { _projectRepositoryMock = new Mock(); _unitOfWorkMock = new Mock(); _currentUserServiceMock = new Mock(); _handler = new CreateProjectCommandHandler( _projectRepositoryMock.Object, _unitOfWorkMock.Object, _currentUserServiceMock.Object, Mock.Of(), Mock.Of>() ); } [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(), default)) .ReturnsAsync((Project?)null); // Act var result = await _handler.Handle(command, default); // Assert result.Should().NotBeNull(); _projectRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), 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 act = async () => await _handler.Handle(command, default); // Assert await act.Should().ThrowAsync() .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> { private readonly HttpClient _client; private readonly ColaFlowWebApplicationFactory _factory; public ProjectsApiTests(ColaFlowWebApplicationFactory 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(); 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(); 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 opencover 80 line,branch,method total ``` ### 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