Skip to content

Testing Strategy

Comprehensive testing approach for the RCIIS DevOps platform, covering unit tests, integration tests, and end-to-end validation.

Overview

The testing strategy ensures code quality, system reliability, and deployment confidence through automated testing at multiple levels.

Testing Pyramid

Unit Tests

  • Scope: Individual functions and classes
  • Coverage: >80% code coverage target
  • Framework: xUnit for .NET applications
  • Execution: Developer workstations and CI pipelines

Integration Tests

  • Scope: Service interactions and database operations
  • Environment: Test containers and test databases
  • Framework: TestContainers for infrastructure dependencies
  • Execution: CI pipelines and staging environment

End-to-End Tests

  • Scope: Complete user workflows and system functionality
  • Environment: Staging environment with production-like data
  • Framework: Postman/Newman for API testing
  • Execution: Staging deployment validation

Unit Testing

.NET Testing Framework

// Example unit test structure
[TestFixture]
public class DeclarationServiceTests
{
    private Mock<IDeclarationRepository> _mockRepository;
    private Mock<IEventPublisher> _mockEventPublisher;
    private DeclarationService _service;

    [SetUp]
    public void Setup()
    {
        _mockRepository = new Mock<IDeclarationRepository>();
        _mockEventPublisher = new Mock<IEventPublisher>();
        _service = new DeclarationService(_mockRepository.Object, _mockEventPublisher.Object);
    }

    [Test]
    public async Task CreateDeclaration_ValidInput_ReturnsSuccess()
    {
        // Arrange
        var declaration = new Declaration { Id = 1, Status = "Draft" };
        _mockRepository.Setup(r => r.CreateAsync(It.IsAny<Declaration>()))
                      .ReturnsAsync(declaration);

        // Act
        var result = await _service.CreateDeclarationAsync(declaration);

        // Assert
        Assert.That(result.IsSuccess, Is.True);
        Assert.That(result.Data.Id, Is.EqualTo(1));

        _mockEventPublisher.Verify(p => p.PublishAsync(
            It.Is<DeclarationCreatedEvent>(e => e.DeclarationId == 1)), 
            Times.Once);
    }

    [Test]
    public async Task CreateDeclaration_InvalidInput_ReturnsError()
    {
        // Arrange
        var declaration = new Declaration(); // Invalid - missing required fields

        // Act & Assert
        var exception = await Assert.ThrowsAsync<ValidationException>(
            () => _service.CreateDeclarationAsync(declaration));

        Assert.That(exception.Message, Contains.Substring("required"));
    }
}

Test Configuration

<!-- Test project configuration -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
    <PackageReference Include="NUnit" Version="3.13.3" />
    <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
    <PackageReference Include="Moq" Version="4.18.4" />
    <PackageReference Include="FluentAssertions" Version="6.10.0" />
    <PackageReference Include="Testcontainers" Version="3.2.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="../Nucleus.API/Nucleus.API.csproj" />
  </ItemGroup>
</Project>

Integration Testing

Database Integration Tests

[TestFixture]
public class DeclarationRepositoryIntegrationTests
{
    private MsSqlContainer _sqlContainer;
    private NucleusDbContext _context;
    private DeclarationRepository _repository;

    [OneTimeSetUp]
    public async Task OneTimeSetUp()
    {
        _sqlContainer = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2019-latest")
            .WithPassword("Test123!")
            .Build();

        await _sqlContainer.StartAsync();

        var connectionString = _sqlContainer.GetConnectionString();
        var options = new DbContextOptionsBuilder<NucleusDbContext>()
            .UseSqlServer(connectionString)
            .Options;

        _context = new NucleusDbContext(options);
        await _context.Database.EnsureCreatedAsync();

        _repository = new DeclarationRepository(_context);
    }

    [OneTimeTearDown]
    public async Task OneTimeTearDown()
    {
        await _context.DisposeAsync();
        await _sqlContainer.DisposeAsync();
    }

    [Test]
    public async Task CreateDeclaration_SavesToDatabase()
    {
        // Arrange
        var declaration = new Declaration
        {
            DeclarationNumber = "TEST001",
            Status = DeclarationStatus.Draft,
            CreatedAt = DateTime.UtcNow
        };

        // Act
        var result = await _repository.CreateAsync(declaration);

        // Assert
        Assert.That(result.Id, Is.GreaterThan(0));

        var saved = await _repository.GetByIdAsync(result.Id);
        Assert.That(saved.DeclarationNumber, Is.EqualTo("TEST001"));
    }
}

Kafka Integration Tests

[TestFixture]
public class EventPublisherIntegrationTests
{
    private KafkaContainer _kafkaContainer;
    private EventPublisher _publisher;

    [OneTimeSetUp]
    public async Task OneTimeSetUp()
    {
        _kafkaContainer = new KafkaBuilder()
            .WithImage("confluentinc/cp-kafka:7.4.0")
            .Build();

        await _kafkaContainer.StartAsync();

        var config = new ProducerConfig
        {
            BootstrapServers = _kafkaContainer.GetBootstrapAddress()
        };

        _publisher = new EventPublisher(config);
    }

    [OneTimeTearDown]
    public async Task OneTimeTearDown()
    {
        _publisher?.Dispose();
        await _kafkaContainer.DisposeAsync();
    }

    [Test]
    public async Task PublishEvent_SendsToKafka()
    {
        // Arrange
        var eventData = new DeclarationCreatedEvent
        {
            DeclarationId = 123,
            Timestamp = DateTime.UtcNow
        };

        // Act
        await _publisher.PublishAsync("test-topic", eventData);

        // Assert - Verify message was sent (implement consumer verification)
        var consumer = new ConsumerBuilder<string, string>(new ConsumerConfig
        {
            BootstrapServers = _kafkaContainer.GetBootstrapAddress(),
            GroupId = "test-group",
            AutoOffsetReset = AutoOffsetReset.Earliest
        }).Build();

        consumer.Subscribe("test-topic");
        var result = consumer.Consume(TimeSpan.FromSeconds(5));

        Assert.That(result, Is.Not.Null);
        Assert.That(result.Message.Value, Contains.Substring("123"));
    }
}

API Testing

Postman Collection Structure

{
  "info": {
    "name": "RCIIS API Tests",
    "description": "Comprehensive API test suite for RCIIS platform"
  },
  "item": [
    {
      "name": "Authentication",
      "item": [
        {
          "name": "Login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"{{test_username}}\",\n  \"password\": \"{{test_password}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/auth/login",
              "host": ["{{base_url}}"],
              "path": ["api", "auth", "login"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Login successful', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test('Token received', function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData.token).to.be.a('string');",
                  "    pm.collectionVariables.set('auth_token', jsonData.token);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}

Newman CLI Testing

#!/bin/bash
# API test execution script

# Run authentication tests
newman run collections/auth-tests.json \
    --environment environments/staging.json \
    --reporters cli,json \
    --reporter-json-export results/auth-results.json

# Run declaration tests
newman run collections/declaration-tests.json \
    --environment environments/staging.json \
    --reporters cli,json \
    --reporter-json-export results/declaration-results.json

# Check results
if [ $? -eq 0 ]; then
    echo "✅ All API tests passed"
else
    echo "❌ API tests failed"
    exit 1
fi

Performance Testing

Load Testing with k6

// k6 load test script
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  stages: [
    { duration: '2m', target: 10 },  // Ramp up
    { duration: '5m', target: 50 },  // Stay at 50 users
    { duration: '2m', target: 0 },   // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'],    // Error rate under 1%
  },
};

export default function() {
  // Login and get token
  const loginResponse = http.post('https://api.staging.devops.africa/auth/login', {
    username: '[email protected]',
    password: 'TestPassword123!'
  });

  check(loginResponse, {
    'login successful': (r) => r.status === 200,
  });

  const token = loginResponse.json().token;

  // Test API endpoints
  const headers = { Authorization: `Bearer ${token}` };

  const declarationsResponse = http.get('https://api.staging.devops.africa/api/declarations', { headers });
  check(declarationsResponse, {
    'declarations retrieved': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

CI/CD Integration

GitHub Actions Testing Workflow

name: Test Pipeline

on:
  push:
    branches: [ master, develop ]
  pull_request:
    branches: [ master ]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '8.0.x'

    - name: Restore dependencies
      run: dotnet restore

    - name: Build
      run: dotnet build --no-restore

    - name: Run unit tests
      run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"

    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: coverage.xml

  integration-tests:
    runs-on: ubuntu-latest
    services:
      mssql:
        image: mcr.microsoft.com/mssql/server:2019-latest
        env:
          SA_PASSWORD: Test123!
          ACCEPT_EULA: Y
        options: >-
          --health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Test123! -Q 'SELECT 1'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 3

      kafka:
        image: confluentinc/cp-kafka:7.4.0
        env:
          KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
          KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092

    steps:
    - uses: actions/checkout@v3

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '8.0.x'

    - name: Run integration tests
      run: dotnet test src/Tests/Integration --logger trx --results-directory TestResults
      env:
        ConnectionStrings__DefaultConnection: "Server=localhost;Database=TestDB;User Id=sa;Password=Test123!;TrustServerCertificate=true;"
        Kafka__BootstrapServers: "localhost:9092"

  api-tests:
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests]
    if: github.ref == 'refs/heads/master'

    steps:
    - uses: actions/checkout@v3

    - name: Install Newman
      run: npm install -g newman newman-reporter-htmlextra

    - name: Run API tests
      run: |
        newman run tests/api/nucleus-api.postman_collection.json \
          --environment tests/api/staging.postman_environment.json \
          --reporters cli,htmlextra \
          --reporter-htmlextra-export TestResults/api-test-report.html

    - name: Upload test results
      uses: actions/upload-artifact@v3
      with:
        name: api-test-results
        path: TestResults/

Test Data Management

Test Data Generation

// Test data factory
public static class TestDataFactory
{
    public static Declaration CreateValidDeclaration(string declarationNumber = null)
    {
        return new Declaration
        {
            DeclarationNumber = declarationNumber ?? $"TEST{Random.Shared.Next(1000, 9999)}",
            DeclarationType = DeclarationType.Import,
            Status = DeclarationStatus.Draft,
            TotalValue = 1000.00m,
            Currency = "USD",
            CreatedAt = DateTime.UtcNow,
            Items = CreateDeclarationItems(3)
        };
    }

    public static List<DeclarationItem> CreateDeclarationItems(int count)
    {
        return Enumerable.Range(1, count)
            .Select(i => new DeclarationItem
            {
                ItemNumber = i,
                Description = $"Test Item {i}",
                Quantity = Random.Shared.Next(1, 10),
                UnitPrice = Random.Shared.Next(10, 100),
                HsCode = $"1234.56.{i:D2}"
            })
            .ToList();
    }
}

Database Seeding

// Test database seeder
public static class TestDatabaseSeeder
{
    public static async Task SeedAsync(NucleusDbContext context)
    {
        if (await context.Declarations.AnyAsync())
            return; // Already seeded

        var declarations = new[]
        {
            TestDataFactory.CreateValidDeclaration("SEED001"),
            TestDataFactory.CreateValidDeclaration("SEED002"),
            TestDataFactory.CreateValidDeclaration("SEED003")
        };

        context.Declarations.AddRange(declarations);
        await context.SaveChangesAsync();
    }
}

Test Environment Management

Kubernetes Test Environment

# Test environment deployment
apiVersion: v1
kind: Namespace
metadata:
  name: nucleus-test
  labels:
    environment: test

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nucleus-test
  namespace: nucleus-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nucleus-test
  template:
    metadata:
      labels:
        app: nucleus-test
    spec:
      containers:
      - name: nucleus
        image: harbor.devops.africa/rciis/nucleus:test
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: Test
        - name: ConnectionStrings__DefaultConnection
          value: "Server=mssql-test;Database=NucleusTestDB;User Id=sa;Password=Test123!;TrustServerCertificate=true;"
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5

Quality Gates

Code Coverage Requirements

  • Minimum Coverage: 80% for new code
  • Branch Coverage: 70% for critical paths
  • Mutation Testing: 60% mutation score for core logic

Performance Benchmarks

  • API Response Time: 95th percentile < 500ms
  • Database Query Time: Average < 100ms
  • Memory Usage: < 1GB per instance under load

Security Testing

  • SAST: Static analysis security testing
  • Dependency Scanning: Vulnerability assessment
  • Container Scanning: Image security validation

Best Practices

Test Organization

  1. AAA Pattern: Arrange, Act, Assert structure
  2. Descriptive Names: Clear test method naming
  3. Single Responsibility: One assertion per test
  4. Test Independence: No test dependencies

Maintenance

  1. Regular Updates: Keep test dependencies current
  2. Flaky Test Management: Identify and fix unstable tests
  3. Test Documentation: Document complex test scenarios
  4. Continuous Improvement: Regular test effectiveness review

For specific testing implementations, refer to the individual service documentation.