Skip to content

Testing

Belay.NET provides comprehensive testing infrastructure supporting unit tests, integration tests, and hardware validation. This guide covers all testing approaches from mock-based unit tests to full hardware-in-the-loop testing.

Test Architecture Overview

Belay.NET's testing strategy follows a layered approach:

  • Unit Tests: Fast, isolated tests with mocks (no hardware)
  • Subprocess Tests: Integration tests using MicroPython unix port
  • Hardware Tests: End-to-end validation with physical devices
  • Performance Tests: Benchmarking and validation testing

Testing Framework

Belay.NET uses:

  • MSTest: Primary testing framework
  • Moq: Mocking framework for unit tests
  • MicroPython unix port: Hardware-independent integration testing
  • Custom validation tools: Hardware testing infrastructure

Unit Testing

Unit tests validate core functionality without hardware dependencies using mocks.

Setting up Unit Tests

csharp
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Belay.Core;
using Belay.Core.Communication;

[TestClass]
public class DeviceTests
{
    private Mock<IDeviceCommunication> mockCommunication;
    private Device device;

    [TestInitialize]
    public void Setup()
    {
        mockCommunication = new Mock<IDeviceCommunication>();
        // Note: Direct Device constructor with IDeviceCommunication is internal
        // Use Device.FromConnectionString("mock:") for testing
    }

    [TestMethod]
    public async Task ExecuteAsync_ValidCode_ReturnsResult()
    {
        // Arrange
        mockCommunication.Setup(c => c.ExecuteAsync("2 + 2", It.IsAny<CancellationToken>()))
                        .ReturnsAsync("4");

        // Act & Assert - actual unit test patterns depend on internal architecture
        // See existing unit tests in tests/Belay.Tests.Unit/ for current patterns
    }
}

Testing Task Attributes

csharp
[TestClass]
public class TaskAttributeTests
{
    private TestDeviceController controller;
    
    [TestInitialize]
    public void Setup()
    {
        // Use subprocess for Task attribute testing
        var device = Device.FromConnectionString("subprocess:../../micropython/ports/unix/build-standard/micropython");
        controller = new TestDeviceController(device);
    }
    
    [TestMethod]
    public async Task CachedTask_SecondCall_ReturnsCachedResult()
    {
        await controller.ConnectAsync();
        
        // First call - not cached
        var start1 = DateTime.UtcNow;
        var result1 = await controller.GetCachedValueAsync();
        var time1 = (DateTime.UtcNow - start1).TotalMilliseconds;
        
        // Second call - cached
        var start2 = DateTime.UtcNow;
        var result2 = await controller.GetCachedValueAsync();
        var time2 = (DateTime.UtcNow - start2).TotalMilliseconds;
        
        // Assert
        Assert.AreEqual(result1, result2);
        Assert.IsTrue(time2 < time1 / 2, "Cached call should be significantly faster");
    }
    
    [TestMethod]
    public async Task ExclusiveTask_ConcurrentCalls_ExecuteSequentially()
    {
        await controller.ConnectAsync();
        
        var tasks = Enumerable.Range(0, 5).Select(_ => controller.ExclusiveOperationAsync()).ToArray();
        var results = await Task.WhenAll(tasks);
        
        // All tasks should complete successfully without interference
        Assert.AreEqual(5, results.Length);
        Assert.IsTrue(results.All(r => !string.IsNullOrEmpty(r)));
    }
}

public class TestDeviceController
{
    private readonly Device device;
    
    public TestDeviceController(Device device)
    {
        this.device = device;
    }
    
    public async Task ConnectAsync() => await device.ConnectAsync();
    
    [Task(Cache = true)]
    public async Task<string> GetCachedValueAsync()
    {
        return await device.ExecuteAsync<string>(@"
import time
time.sleep_ms(100)  # Simulate work
'cached_result_' + str(time.ticks_ms())
        ");
    }
    
    [Task(Exclusive = true)]
    public async Task<string> ExclusiveOperationAsync()
    {
        return await device.ExecuteAsync<string>(@"
import time
start = time.ticks_ms()
time.sleep_ms(50)
'exclusive_' + str(start)
        ");
    }
}

Subprocess Testing

Subprocess tests use the MicroPython unix port for hardware-independent integration testing.

Setting up Subprocess Tests

  1. Build MicroPython unix port:
bash
cd micropython/ports/unix
make submodules
make
  1. Create subprocess test:
csharp
[TestClass]
[TestCategory("Subprocess")]
public class SubprocessIntegrationTests
{
    private Device device;

    [TestInitialize]
    public async Task Setup()
    {
        var micropythonPath = Path.GetFullPath("../../micropython/ports/unix/build-standard/micropython");
        device = Device.FromConnectionString($"subprocess:{micropythonPath}");
        await device.ConnectAsync();
    }

    [TestCleanup]
    public async Task Cleanup()
    {
        await device?.DisconnectAsync();
        device?.Dispose();
    }

    [TestMethod]
    public async Task BasicExecution_ReturnsCorrectResult()
    {
        var result = await device.ExecuteAsync<int>("2 + 2");
        Assert.AreEqual(4, result);
    }

    [TestMethod]
    public async Task PythonModules_ImportSuccessfully()
    {
        var result = await device.ExecuteAsync<string>(@"
import sys
import json
import time
'modules_loaded'
        ");
        Assert.AreEqual("modules_loaded", result);
    }

    [TestMethod]
    public async Task FileOperations_WorkCorrectly()
    {
        // Test basic file operations
        await device.ExecuteAsync(@"
with open('/tmp/test.txt', 'w') as f:
    f.write('Hello World')
        ");
        
        var content = await device.ExecuteAsync<string>(@"
with open('/tmp/test.txt', 'r') as f:
    f.read()
        ");
        
        Assert.AreEqual("Hello World", content);
    }
}

Running Subprocess Tests

bash
# Run only subprocess tests
dotnet test --filter "TestCategory=Subprocess"

# Run with verbose output
dotnet test --filter "TestCategory=Subprocess" --logger "console;verbosity=detailed"

Hardware Testing

Hardware tests validate functionality with real MicroPython devices.

Hardware Test Setup

  1. Connect devices: ESP32, Raspberry Pi Pico, or other MicroPython boards
  2. Identify connection strings: Use device manager or /dev/tty* to find ports
  3. Configure test environment: Set up test configuration

Example Hardware Tests

csharp
[TestClass]
[TestCategory("Hardware")]
[TestCategory("ESP32")]
public class ESP32HardwareTests
{
    private Device device;
    private ESP32Controller controller;
    
    [TestInitialize]
    public async Task Setup()
    {
        // Use environment variable or skip test if not available
        var connectionString = Environment.GetEnvironmentVariable("BELAY_ESP32_CONNECTION");
        if (string.IsNullOrEmpty(connectionString))
        {
            Assert.Inconclusive("ESP32 not available - set BELAY_ESP32_CONNECTION environment variable");
        }
        
        device = Device.FromConnectionString(connectionString);
        await device.ConnectAsync();
        
        controller = new ESP32Controller(device);
        await controller.InitializeESP32Async();
    }
    
    [TestCleanup]
    public async Task Cleanup()
    {
        await device?.DisconnectAsync();
        device?.Dispose();
    }
    
    [TestMethod]
    public async Task LED_CanControl()
    {
        await controller.SetLedAsync(true);
        await Task.Delay(500);
        await controller.SetLedAsync(false);
        // LED control doesn't return status, just verify no exception
    }
    
    [TestMethod]
    public async Task ADC_ReturnsValidRange()
    {
        var reading = await controller.ReadADCAsync();
        Assert.IsTrue(reading >= 0 && reading <= 4095, "ADC reading should be 0-4095 for 12-bit ADC");
    }
    
    [TestMethod]
    public async Task WiFi_IsAvailable()
    {
        var available = await controller.CheckWiFiAvailabilityAsync();
        Assert.IsTrue(available, "ESP32 should have WiFi capability");
    }
}

Running Hardware Tests

bash
# Set up environment variables
export BELAY_ESP32_CONNECTION="serial:/dev/ttyUSB0"
export BELAY_PICO_CONNECTION="serial:/dev/ttyACM0"

# Run hardware tests
dotnet test --filter "TestCategory=Hardware"

# Run specific platform tests
dotnet test --filter "TestCategory=ESP32"
dotnet test --filter "TestCategory=Pico"

Performance Testing

Performance tests validate timing characteristics and throughput.

csharp
[TestClass]
[TestCategory("Performance")]
public class PerformanceTests
{
    private Device device;
    
    [TestInitialize]
    public async Task Setup()
    {
        // Use subprocess for consistent performance measurement
        var micropythonPath = Path.GetFullPath("../../micropython/ports/unix/build-standard/micropython");
        device = Device.FromConnectionString($"subprocess:{micropythonPath}");
        await device.ConnectAsync();
    }
    
    [TestMethod]
    public async Task SimpleExecution_CompletesWithinTimeout()
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        
        var result = await device.ExecuteAsync<int>("2 + 2");
        
        stopwatch.Stop();
        
        Assert.AreEqual(4, result);
        Assert.IsTrue(stopwatch.ElapsedMilliseconds < 1000, "Simple execution should complete within 1 second");
    }
    
    [TestMethod]
    public async Task TaskCaching_ImprovesPerformance()
    {
        var controller = new TestDeviceController(device);
        
        // Measure first call (uncached)
        var stopwatch1 = System.Diagnostics.Stopwatch.StartNew();
        var result1 = await controller.GetCachedValueAsync();
        stopwatch1.Stop();
        
        // Measure second call (cached)
        var stopwatch2 = System.Diagnostics.Stopwatch.StartNew();
        var result2 = await controller.GetCachedValueAsync();
        stopwatch2.Stop();
        
        Assert.AreEqual(result1, result2);
        Assert.IsTrue(stopwatch2.ElapsedMilliseconds < stopwatch1.ElapsedMilliseconds / 2,
            "Cached call should be at least 2x faster");
    }
    
    [TestMethod]
    public async Task ConcurrentExecution_HandlesLoad()
    {
        var tasks = Enumerable.Range(0, 10)
            .Select(i => device.ExecuteAsync<int>($"{i} * 2"))
            .ToArray();
        
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        var results = await Task.WhenAll(tasks);
        stopwatch.Stop();
        
        // Verify results
        for (int i = 0; i < 10; i++)
        {
            Assert.AreEqual(i * 2, results[i]);
        }
        
        // Should complete within reasonable time
        Assert.IsTrue(stopwatch.ElapsedMilliseconds < 10000, "10 concurrent operations should complete within 10 seconds");
    }
}

Test Configuration

Configure tests for different environments and scenarios.

Environment Variables

bash
# Hardware connections
export BELAY_ESP32_CONNECTION="serial:/dev/ttyUSB0"
export BELAY_PICO_CONNECTION="serial:/dev/ttyACM0"
export BELAY_MICROPYTHON_PATH="../../micropython/ports/unix/build-standard/micropython"

# Test settings
export BELAY_TEST_TIMEOUT_MS="30000"
export BELAY_TEST_LOG_LEVEL="Debug"

Test Configuration Class

csharp
public static class TestConfiguration
{
    public static string ESP32Connection => Environment.GetEnvironmentVariable("BELAY_ESP32_CONNECTION") ?? "";
    public static string PicoConnection => Environment.GetEnvironmentVariable("BELAY_PICO_CONNECTION") ?? "";
    public static string MicroPythonPath => Environment.GetEnvironmentVariable("BELAY_MICROPYTHON_PATH") ?? 
        Path.GetFullPath("../../micropython/ports/unix/build-standard/micropython");
    
    public static int TestTimeoutMs => int.Parse(Environment.GetEnvironmentVariable("BELAY_TEST_TIMEOUT_MS") ?? "10000");
    
    public static bool HasESP32Hardware => !string.IsNullOrEmpty(ESP32Connection);
    public static bool HasPicoHardware => !string.IsNullOrEmpty(PicoConnection);
    
    public static void SkipIfNoHardware(string platform)
    {
        var hasHardware = platform.ToLower() switch
        {
            "esp32" => HasESP32Hardware,
            "pico" => HasPicoHardware,
            _ => false
        };
        
        if (!hasHardware)
        {
            Assert.Inconclusive($"{platform} hardware not available - configure BELAY_{platform.ToUpper()}_CONNECTION");
        }
    }
}

Custom Test Attributes

csharp
[AttributeUsage(AttributeTargets.Method)]
public class HardwareTestAttribute : TestCategoryAttribute
{
    public HardwareTestAttribute(string platform) : base("Hardware")
    {
        Platform = platform;
    }
    
    public string Platform { get; }
}

// Usage
[TestMethod]
[HardwareTest("ESP32")]
public async Task ESP32_SpecificTest()
{
    TestConfiguration.SkipIfNoHardware("ESP32");
    // Test implementation
}

Test Execution

Running All Tests

bash
# Run all tests
dotnet test

# Run with coverage
dotnet test --collect:"XPlat Code Coverage"

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

Running Specific Test Categories

bash
# Unit tests only (fast)
dotnet test --filter "TestCategory!=Integration&TestCategory!=Hardware&TestCategory!=Subprocess"

# Subprocess integration tests
dotnet test --filter "TestCategory=Subprocess"

# Hardware tests (requires connected devices)
dotnet test --filter "TestCategory=Hardware"

# Specific platform hardware tests
dotnet test --filter "TestCategory=ESP32"
dotnet test --filter "TestCategory=Pico"

# Performance tests
dotnet test --filter "TestCategory=Performance"

Test Project Structure

Belay.NET includes several test projects:

tests/
├── Belay.Tests.Unit/           # Unit tests with mocks
├── Belay.Tests.Integration/    # Integration tests
├── Belay.Tests.Subprocess/     # Subprocess-based tests
└── Belay.Tests.Performance/    # Performance validation

Continuous Integration

Example CI configuration for different test types:

yaml
name: Belay.NET Tests

on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          
      - 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 tests/Belay.Tests.Unit/ \
            --no-build --verbosity normal \
            --logger trx --collect:"XPlat Code Coverage"
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: '**/coverage.cobertura.xml'

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '8.0.x'
      
      - name: Install build dependencies
        run: sudo apt-get update && sudo apt-get install -y build-essential
      
      - name: Build MicroPython unix port
        run: |
          cd micropython/ports/unix
          make submodules
          make -j$(nproc)
      
      - name: Run subprocess integration tests
        run: |
          dotnet test tests/Belay.Tests.Subprocess/ \
            --verbosity normal --logger trx
            
      - name: Run integration tests
        run: |
          dotnet test tests/Belay.Tests.Integration/ \
            --verbosity normal --logger trx

  hardware-tests:
    runs-on: [self-hosted, belay-hardware]
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          
      - name: Run ESP32 hardware tests
        run: |
          export BELAY_ESP32_CONNECTION="serial:/dev/ttyUSB0"
          dotnet test --filter "TestCategory=ESP32" --logger trx
          
      - name: Run Pico hardware tests
        run: |
          export BELAY_PICO_CONNECTION="serial:/dev/ttyACM0"
          dotnet test --filter "TestCategory=Pico" --logger trx

Validation Tools

Belay.NET includes validation tools for comprehensive testing:

Protocol Comparison Tool

bash
# Compare subprocess vs hardware protocol behavior
dotnet run --project examples/ProtocolComparison/ProtocolComparison.csproj \
  ../../micropython/ports/unix/build-standard/micropython \
  serial:/dev/ttyUSB0

Platform Comparison Tool

bash
# Compare ESP32 vs Pico performance
dotnet run --project examples/PlatformComparisonTest/PlatformComparisonTest.csproj \
  serial:/dev/ttyUSB0 serial:/dev/ttyACM0

Hardware Validation Tools

bash
# ESP32 hardware validation
dotnet run --project examples/ESP32HardwareTest/ESP32HardwareTest.csproj serial:/dev/ttyUSB0

# Pico hardware validation
dotnet run --project examples/PicoHardwareTest/PicoHardwareTest.csproj serial:/dev/ttyACM0

Test Data Helpers

csharp
public static class TestHelpers
{
    public static string GetValidPythonCode() => "2 + 2";
    
    public static string GetInvalidPythonCode() => "this is not valid python syntax";
    
    public static string GetLargePythonCode(int lines = 100) => 
        string.Join("\n", Enumerable.Range(1, lines).Select(i => $"var{i} = {i} * 2"));
    
    public static byte[] GetTestBinaryData(int size = 1024)
    {
        var data = new byte[size];
        Random.Shared.NextBytes(data);
        return data;
    }
    
    public static string GetTestJsonData() => 
        """
        {
            "temperature": 25.5,
            "humidity": 60.2,
            "timestamp": "2025-01-01T12:00:00Z"
        }
        """;
        
    public static async Task WaitForDeviceAsync(Device device, int timeoutMs = 5000)
    {
        var cancellation = new CancellationTokenSource(timeoutMs);
        while (!cancellation.Token.IsCancellationRequested)
        {
            try
            {
                await device.ExecuteAsync("1", cancellation.Token);
                return;
            }
            catch
            {
                await Task.Delay(100, cancellation.Token);
            }
        }
        throw new TimeoutException("Device did not become ready within timeout");
    }
}

Best Practices

Test Organization

  1. Separate test projects by scope (unit, integration, hardware)
  2. Use test categories for filtering (TestCategory attribute)
  3. Environment detection for hardware availability
  4. Proper cleanup in TestCleanup methods

Performance Testing

  1. Use subprocess for consistent performance measurement
  2. Measure multiple runs and calculate averages
  3. Set reasonable timeouts based on expected performance
  4. Test cached vs uncached operations separately

Hardware Testing

  1. Check availability before running hardware tests
  2. Use environment variables for connection strings
  3. Reset device state between tests
  4. Handle platform differences in test expectations

Error Handling

  1. Test both success and failure paths
  2. Validate exception types and messages
  3. Test timeout scenarios
  4. Verify resource cleanup after errors

Running Tests Locally

Prerequisites

  1. .NET 8.0 SDK installed
  2. MicroPython unix port built (for subprocess tests)
  3. Hardware devices connected (for hardware tests)
  4. Environment variables configured

Quick Start

bash
# Clone and build
git clone --recursive https://github.com/belay-dotnet/Belay.NET.git
cd Belay.NET

# Build MicroPython unix port
cd micropython/ports/unix
make submodules && make
cd ../../..

# Run unit tests
dotnet test tests/Belay.Tests.Unit/

# Run subprocess tests
dotnet test tests/Belay.Tests.Subprocess/

# Run with hardware (if available)
export BELAY_ESP32_CONNECTION="serial:/dev/ttyUSB0"
dotnet test --filter "TestCategory=ESP32"

External Resources

Need help? Check our test examples or ask in GitHub Discussions.

Released under the Apache License 2.0.