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: 'test@example.com',
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¶
- AAA Pattern: Arrange, Act, Assert structure
- Descriptive Names: Clear test method naming
- Single Responsibility: One assertion per test
- Test Independence: No test dependencies
Maintenance¶
- Regular Updates: Keep test dependencies current
- Flaky Test Management: Identify and fix unstable tests
- Test Documentation: Document complex test scenarios
- Continuous Improvement: Regular test effectiveness review
For specific testing implementations, refer to the individual service documentation.