Skip to content

Peer Service Integration Testing Guide ​

Complete guide for running, writing, and maintaining integration tests for the Sorcha Peer Service.

Table of Contents ​

  1. Overview
  2. Getting Started
  3. Test Architecture
  4. Running Tests
  5. Test Suites
  6. Writing Tests
  7. Performance Testing
  8. Troubleshooting
  9. CI/CD Integration
  10. Best Practices

Overview ​

The Sorcha Peer Service integration test suite provides comprehensive end-to-end testing of peer-to-peer functionality using real service instances. Unlike unit tests that mock dependencies, these tests validate actual network communication, gRPC streaming, and multi-peer interactions.

What We Test ​

  • ✅ Peer Discovery: Registration, lookup, and peer list management
  • ✅ P2P Communication: Bidirectional gRPC streaming between peers
  • ✅ Throughput: High-volume transaction processing (1000+ tx/sec)
  • ✅ Performance: Memory usage, latency, and sustained load
  • ✅ Health & Metrics: Service health checks and metrics accuracy

Test Statistics ​

  • Total Tests: 36+
  • Test Categories: 4 (Discovery, Communication, Throughput, Health)
  • Execution Time: ~50 seconds (full suite)
  • Lines of Test Code: 2000+

Getting Started ​

Prerequisites ​

  1. .NET 10.0 SDK or later
  2. Ports 5000-6000 available (tests use random ports in this range)
  3. Visual Studio 2025, Rider, or VS Code (optional)

Quick Start ​

bash
# Clone and navigate to test directory
cd tests/Sorcha.Peer.Service.Integration.Tests

# Run all tests
dotnet test

# Or use the helper script (Windows)
.\run-integration-tests.ps1

# Or use the helper script (Linux/macOS)
./run-integration-tests.sh

✅ Expected: All tests pass in ~50 seconds


Test Architecture ​

Design Pattern: WebApplicationFactory ​

We use ASP.NET Core's WebApplicationFactory<T> to create real, in-memory service instances:

┌─────────────────────────────────────────┐
│        PeerTestFixture (xUnit)          │
│  Manages lifecycle of 3+ peer instances │
└────────────┬────────────────────────────┘
             │
     ┌───────┴────────┬────────────┐
     │                │            │
┌────▼─────┐    ┌────▼─────┐  ┌──▼──────┐
│ Peer 1   │    │ Peer 2   │  │ Peer 3  │
│ HTTP+gRPC│    │ HTTP+gRPC│  │HTTP+gRPC│
└──────────┘    └──────────┘  └─────────┘

Key Components ​

1. PeerTestFixture ​

Location: Infrastructure/PeerTestFixture.cs

Manages multiple peer service instances:

csharp
public class PeerTestFixture : IAsyncLifetime
{
    public List<PeerInstance> Peers { get; }  // 3 peers by default

    public async Task<PeerInstance> AddPeerInstanceAsync(string peerId);
    public async Task InitializeAsync();
    public async Task DisposeAsync();
}

Features:

  • Creates isolated test instances
  • Provides HTTP and gRPC clients for each peer
  • Handles graceful startup and shutdown
  • Shared across all tests in a collection

2. PeerServiceFactory ​

Location: Infrastructure/PeerServiceFactory.cs

Custom WebApplicationFactory for service configuration:

csharp
public class PeerServiceFactory : WebApplicationFactory<Program>
{
    public string PeerId { get; set; }
    public int Port { get; set; }
}

Customizations:

  • Replaces Redis with in-memory cache
  • Uses random ports to avoid conflicts
  • Configures Testing environment
  • Removes Aspire dependencies for standalone testing

3. TestHelpers ​

Location: Infrastructure/TestHelpers.cs

Utility functions for common operations:

csharp
public static class TestHelpers
{
    public static byte[] CreateRandomPayload(int sizeInBytes);
    public static string GenerateTransactionId();
    public static Task<bool> WaitForConditionAsync(...);
    public static StringContent ToJsonContent<T>(this T obj);
}

Test Collection ​

All tests use the [Collection("PeerIntegration")] attribute to share the same PeerTestFixture instance, improving performance by reusing peer instances across tests.


Running Tests ​

Basic Commands ​

bash
# Run all tests
dotnet test

# Run with detailed output
dotnet test --logger "console;verbosity=detailed"

# Run specific test class
dotnet test --filter "FullyQualifiedName~PeerDiscoveryTests"

# Run specific test method
dotnet test --filter "RegisterPeer_Via_REST_Should_Return_Success"

# Run tests in parallel (default)
dotnet test --parallel

# Run tests sequentially
dotnet test --parallel none

Using Helper Scripts ​

PowerShell (Windows) ​

powershell
# Basic run
.\run-integration-tests.ps1

# Run with coverage
.\run-integration-tests.ps1 -Coverage

# Run specific suite
.\run-test-suite.ps1 discovery

# Run with verbose output
.\run-integration-tests.ps1 -Verbose

# Run in watch mode (auto-rerun on changes)
.\run-integration-tests.ps1 -Watch

Bash (Linux/macOS) ​

bash
# Basic run
./run-integration-tests.sh

# Run with coverage
./run-integration-tests.sh --coverage

# Run with verbose output
./run-integration-tests.sh --verbose

# Disable parallel execution
./run-integration-tests.sh --no-parallel

Visual Studio / Rider ​

  1. Open Test Explorer
  2. Right-click Sorcha.Peer.Service.Integration.Tests
  3. Select Run or Debug
  4. Use filters to run specific categories

Test Suites ​

1. Peer Discovery Tests (PeerDiscoveryTests.cs) ​

Purpose: Validate peer registration, discovery, and management.

Test Count: 12 tests Execution Time: ~5 seconds

Key Tests ​

TestValidates
RegisterPeer_Via_REST_Should_Return_SuccessREST API registration
RegisterPeer_Via_gRPC_Should_Return_SuccessgRPC registration
RegisterPeer_Without_PeerId_Should_Generate_IdAuto-generated peer IDs
GetAllPeers_Should_Return_Registered_PeersPeer listing
Multiple_Peers_Can_Discover_Each_OtherCross-peer discovery
UnregisterPeer_Should_Remove_From_RegistryPeer cleanup

Example Test ​

csharp
[Fact]
public async Task RegisterPeer_Via_REST_Should_Return_Success()
{
    // Arrange
    var peer = _fixture.Peers[0];
    var newPeer = new PeerNode
    {
        PeerId = "test-peer",
        Endpoint = "http://localhost:5001",
        Metadata = new Dictionary<string, string> { ["region"] = "us-west" }
    };

    // Act
    var response = await peer.HttpClient.PostAsJsonAsync("/api/peers", newPeer);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.Created);
    var registered = await response.Content.ReadFromJsonAsync<PeerNode>();
    registered!.PeerId.Should().Be("test-peer");
}

2. Peer Communication Tests (PeerCommunicationTests.cs) ​

Purpose: Validate peer-to-peer messaging and streaming.

Test Count: 10 tests Execution Time: ~10 seconds

Key Tests ​

TestValidates
Single_Transaction_Should_Be_Processed_SuccessfullyBasic message exchange
Multiple_Sequential_Transactions_Should_Be_ProcessedSequential messaging
Large_Payload_Transaction_Should_Be_Handled1 MB message handling
Bidirectional_Stream_Should_Work_CorrectlyFull-duplex streaming
Concurrent_Streams_From_Same_Peer_Should_WorkMultiple streams per peer
Transaction_Processing_Should_Update_MetricsMetrics tracking

Example Test ​

csharp
[Fact]
public async Task Single_Transaction_Should_Be_Processed_Successfully()
{
    // Arrange
    var peer = _fixture.Peers[0];
    var transactionId = TestHelpers.GenerateTransactionId();

    // Act
    using var call = peer.GrpcClient.StreamTransactions();
    await call.RequestStream.WriteAsync(new TransactionMessage
    {
        TransactionId = transactionId,
        FromPeer = peer.PeerId,
        ToPeer = "destination",
        Payload = Google.Protobuf.ByteString.CopyFrom(
            TestHelpers.CreateRandomPayload(512)),
        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
    });
    await call.RequestStream.CompleteAsync();

    var response = await call.ResponseStream.ReadAllAsync().FirstAsync();

    // Assert
    response.Success.Should().BeTrue();
    response.TransactionId.Should().Be(transactionId);
}

3. Peer Throughput Tests (PeerThroughputTests.cs) ​

Purpose: Validate performance under load.

Test Count: 8 tests Execution Time: ~30 seconds

Performance Targets ​

MetricTargetTest
Small payload throughput>100 tx/secHigh_Volume_Transaction_Stream_Should_Maintain_Performance
Large payload throughput>1 MB/secLarge_Payload_Throughput_Test
Performance degradation<30%Sustained_Load_Should_Not_Degrade_Performance
Memory growth<50%Memory_Usage_Should_Remain_Stable_Under_Load

Key Tests ​

TestLoad Profile
High_Volume_Transaction_Stream_Should_Maintain_Performance1000 transactions @ 512 bytes
Large_Payload_Throughput_Test100 transactions @ 10 KB
Parallel_Peer_Throughput_Test3 peers × 200 transactions
Burst_Traffic_Should_Be_Handled_Gracefully3 bursts × 500 transactions

Example Test Output ​

Throughput: 234.56 transactions/second
Total time: 4.27 seconds
Average latency: 4.26 ms
Memory usage: 45.23 MB
✓ System should handle at least 100 transactions per second

4. Peer Health Tests (PeerHealthTests.cs) ​

Purpose: Validate health checks and metrics reporting.

Test Count: 6 tests Execution Time: ~5 seconds

Key Tests ​

TestValidates
Health_Endpoint_Should_Return_Healthy_Status/api/health availability
Metrics_Endpoint_Should_Return_Current_Metrics/api/metrics accuracy
Metrics_Via_gRPC_Should_Match_REST_MetricsREST/gRPC consistency
Uptime_Should_Increase_Over_TimeUptime tracking

Writing Tests ​

Test Template ​

csharp
[Fact]
public async Task MyTest_Should_Expected_Behavior()
{
    // Arrange - Set up test data and preconditions
    var peer = _fixture.Peers[0];
    var testData = CreateTestData();

    // Act - Execute the operation under test
    var result = await peer.GrpcClient.SomeOperation(testData);

    // Assert - Verify expected outcomes
    result.Should().NotBeNull();
    result.Success.Should().BeTrue();
}

Best Practices ​

1. Use Descriptive Test Names ​

✅ Good:

csharp
RegisterPeer_Via_REST_Should_Return_Success

❌ Bad:

csharp
Test1
RegisterPeerTest

2. Use FluentAssertions ​

✅ Good:

csharp
response.StatusCode.Should().Be(HttpStatusCode.OK);
peers.Should().HaveCountGreaterThan(0);

❌ Bad:

csharp
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(peers.Count > 0);

3. Clean Up Resources ​

csharp
[Fact]
public async Task My_Test()
{
    // Use 'using' for disposable resources
    using var call = peer.GrpcClient.StreamTransactions();

    // Test logic...

    // Explicit cleanup if needed
    await call.RequestStream.CompleteAsync();
}

4. Isolate Test Data ​

csharp
// ✅ Good - Unique IDs prevent conflicts
var peerId = $"test-{Guid.NewGuid():N}";

// ❌ Bad - Hardcoded IDs may conflict
var peerId = "test-peer";

5. Test One Thing ​

✅ Good:

csharp
[Fact]
public async Task RegisterPeer_Should_Return_Created_Status()
{
    var response = await peer.HttpClient.PostAsJsonAsync("/api/peers", newPeer);
    response.StatusCode.Should().Be(HttpStatusCode.Created);
}

[Fact]
public async Task RegisterPeer_Should_Return_Peer_With_Id()
{
    var response = await peer.HttpClient.PostAsJsonAsync("/api/peers", newPeer);
    var registered = await response.Content.ReadFromJsonAsync<PeerNode>();
    registered!.PeerId.Should().NotBeNullOrEmpty();
}

❌ Bad:

csharp
[Fact]
public async Task RegisterPeer_Tests()
{
    // Testing multiple things in one test
    response.StatusCode.Should().Be(HttpStatusCode.Created);
    registered!.PeerId.Should().NotBeNullOrEmpty();
    registered.Endpoint.Should().NotBeNullOrEmpty();
    // ... etc
}

Performance Testing ​

Measuring Throughput ​

csharp
var stopwatch = Stopwatch.StartNew();

// Send transactions
for (int i = 0; i < transactionCount; i++)
{
    await call.RequestStream.WriteAsync(transaction);
}

stopwatch.Stop();

var throughput = transactionCount / stopwatch.Elapsed.TotalSeconds;
Console.WriteLine($"Throughput: {throughput:F2} tx/sec");

Performance Assertions ​

csharp
// Minimum throughput
throughput.Should().BeGreaterThan(100,
    "System should handle at least 100 tx/sec");

// Maximum degradation
lastBatchThroughput.Should().BeGreaterThan(firstBatchThroughput * 0.7,
    "Performance should not degrade more than 30%");

// Memory stability
memoryGrowthPercent.Should().BeLessThan(50,
    "Memory usage should remain stable");

Troubleshooting ​

Common Issues ​

1. Port Conflicts ​

Symptom:

System.Net.Sockets.SocketException: Address already in use

Solution:

bash
# Windows
netstat -ano | findstr "5xxx"
taskkill /PID <pid> /F

# Linux/macOS
lsof -i :5xxx
kill -9 <pid>

2. Test Timeouts ​

Symptom:

Test exceeded timeout of 120000ms

Solutions:

  • Run sequentially: dotnet test --parallel none
  • Increase timeout: await task.WaitAsync(TimeSpan.FromMinutes(5))
  • Check system resources

3. gRPC Errors ​

Symptom:

RpcException: Status(StatusCode="Unavailable")

Solutions:

  • Ensure HTTP/2 is enabled (automatic in .NET 10)
  • Check firewall/antivirus settings
  • Verify Protocol Buffers are generated

4. Random Test Failures ​

Symptom: Tests pass individually but fail when run together

Solutions:

  • Check for shared state between tests
  • Ensure proper cleanup in Dispose
  • Use unique test data IDs

CI/CD Integration ​

GitHub Actions ​

.github/workflows/integration-tests.yml:

yaml
name: Integration Tests

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

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      - name: Restore dependencies
        run: dotnet restore

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

      - name: Run Integration Tests
        run: |
          cd tests/Sorcha.Peer.Service.Integration.Tests
          dotnet test --no-build --logger "trx;LogFileName=test-results.trx" \
            --collect:"XPlat Code Coverage"

      - name: Upload Test Results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: '**/test-results.trx'

      - name: Upload Coverage
        uses: codecov/codecov-action@v4
        with:
          files: '**/coverage.cobertura.xml'

Azure DevOps ​

azure-pipelines.yml:

yaml
trigger:
  branches:
    include:
      - master
      - develop

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: UseDotNet@2
  inputs:
    version: '10.0.x'

- task: DotNetCoreCLI@2
  displayName: 'Restore'
  inputs:
    command: 'restore'

- task: DotNetCoreCLI@2
  displayName: 'Build'
  inputs:
    command: 'build'
    arguments: '--no-restore'

- task: DotNetCoreCLI@2
  displayName: 'Run Integration Tests'
  inputs:
    command: 'test'
    projects: 'tests/Sorcha.Peer.Service.Integration.Tests/*.csproj'
    arguments: '--no-build --logger trx --collect:"XPlat Code Coverage"'

- task: PublishTestResults@2
  condition: always()
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '**/*.trx'

- task: PublishCodeCoverageResults@2
  inputs:
    summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'

Best Practices ​

1. Test Organization ​

  • ✅ Group related tests in the same class
  • ✅ Use [Theory] for parameterized tests
  • ✅ Keep tests focused and independent
  • ✅ Use descriptive test names

2. Performance Testing ​

  • ✅ Output performance metrics to console
  • ✅ Set realistic performance targets
  • ✅ Test sustained load, not just bursts
  • ✅ Monitor memory usage

3. Assertions ​

  • ✅ Use FluentAssertions for readability
  • ✅ Include descriptive failure messages
  • ✅ Assert on specific values, not just "not null"
  • ✅ Verify both success and error cases

4. Maintenance ​

  • ✅ Keep tests fast (aim for <1 min total)
  • ✅ Avoid flaky tests (timing-dependent logic)
  • ✅ Update tests when APIs change
  • ✅ Maintain documentation

5. Coverage ​

  • ✅ Aim for >80% code coverage
  • ✅ Cover happy paths and error cases
  • ✅ Test edge cases and boundary conditions
  • ✅ Test concurrent scenarios

Additional Resources ​


Happy Testing! 🎉

Released under the MIT License.