Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GitHub

This documentation is part of the "Projects with Books" initiative at zenOSmosis.

The source code for this project is available on GitHub.

Testing

Relevant source files

Purpose and Scope

This document provides an overview of testing strategies and patterns used in the rust-muxio codebase. It covers the testing philosophy, test organization, common testing patterns, and available testing utilities. For detailed information about specific testing approaches, see Unit Testing and Integration Testing.

The rust-muxio system emphasizes compile-time correctness through shared type definitions and trait-based abstractions. This design philosophy directly influences the testing strategy: many potential bugs are prevented by the type system, allowing tests to focus on runtime behavior, protocol correctness, and cross-platform compatibility.

Testing Philosophy

The rust-muxio testing approach is built on three core principles:

Compile-Time Guarantees Reduce Runtime Test Burden : By using shared service definitions (example-muxio-rpc-service-definition), both clients and servers depend on the same RpcMethodPrebuffered trait implementations. This ensures that parameter encoding/decoding, method IDs, and data structures are consistent at compile time. Tests do not need to verify type mismatches or protocol version incompatibilities—these are caught by the compiler.

Layered Testing Mirrors Layered Architecture : The system's modular design (core multiplexing → RPC abstraction → transport implementations) enables focused testing at each layer. Unit tests verify RpcDispatcher behavior in isolation tests/rpc_dispatcher_tests.rs while integration tests validate the complete stack including network transports extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs

Cross-Platform Validation Is Essential : Because the same RPC service definitions work across Tokio-based native clients, WASM browser clients, and the server, tests must verify that all client types can communicate with the server correctly. This is achieved through parallel integration test suites that use identical test cases against different client implementations.

Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:1-97 tests/rpc_dispatcher_tests.rs:1-30

Test Organization in the Workspace

Test Location Strategy

Tests are organized by scope and purpose:

Test TypeLocationPurposeExample
Core Unit Teststests/ in workspace rootValidate RpcDispatcher logic without async runtimerpc_dispatcher_tests.rs
Integration Teststests/ in extension cratesValidate full client-server communicationprebuffered_integration_tests.rs
Test Utilitiesextensions/muxio-ext-test/Shared test helpers and mock implementationsN/A
Test Service Definitionsexample-muxio-rpc-service-definition/Shared RPC methods for testingAdd, Mult, Echo

This organization ensures that:

  • Core library tests have no async runtime dependencies
  • Extension tests can use their specific runtime environments
  • Test service definitions are reusable across all client types
  • Integration tests exercise the complete, realistic code paths

Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:1-18 tests/rpc_dispatcher_tests.rs:1-7

Integration Test Architecture

Integration tests create realistic client-server scenarios to validate end-to-end behavior. The following diagram illustrates the typical test setup:

Key Components

sequenceDiagram
    participant Test as "Test Function\n#[tokio::test]"
    participant Listener as "TcpListener\nRandom Port"
    participant Server as "Arc<RpcServer>"
    participant Endpoint as "RpcServiceEndpointInterface"
    participant Client as "RpcClient\n(Tokio or WASM)"
    participant ServiceDef as "Add/Mult/Echo\nService Definitions"
    
    Test->>Listener: bind("127.0.0.1:0")
    Test->>Server: RpcServer::new(None)
    Test->>Server: server.endpoint()
    Server-->>Endpoint: endpoint reference
    
    Test->>Endpoint: register_prebuffered(Add::METHOD_ID, handler)
    Test->>Endpoint: register_prebuffered(Mult::METHOD_ID, handler)
    Test->>Endpoint: register_prebuffered(Echo::METHOD_ID, handler)
    
    Test->>Test: tokio::spawn(server.serve_with_listener)
    
    Test->>Client: RpcClient::new(host, port)
    
    Test->>ServiceDef: Add::call(client, params)
    ServiceDef->>Client: call_rpc_buffered(request)
    Client->>Server: WebSocket binary frames
    Server->>Endpoint: dispatch by METHOD_ID
    Endpoint->>Endpoint: execute handler
    Endpoint->>Client: response frames
    Client->>ServiceDef: decode_response
    ServiceDef-->>Test: Result<f64>
    
    Test->>Test: assert_eq!(result, expected)

Random Port Binding : Tests bind to 127.0.0.1:0 to obtain a random available port, preventing conflicts when running multiple tests in parallel extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:21-23

Arc-Wrapped Server : The RpcServer is wrapped in Arc<RpcServer> to enable cloning into spawned tasks while maintaining shared state extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs28

Separate Endpoint Registration : Handlers are registered on the endpoint obtained via server.endpoint(), not directly on the server. This separation allows handler registration to complete before the server starts accepting connections extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:31-61

Background Server Task : The server runs in a spawned Tokio task, allowing the test to proceed with client operations on the main test task extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:64-70

Shared Service Definitions : Both client and server use the same Add, Mult, and Echo implementations from example-muxio-rpc-service-definition, ensuring type-safe, consistent encoding/decoding extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs1

Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:16-97 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:1-20

Common Test Patterns

Success Case Testing

The most fundamental test pattern validates that RPC calls complete successfully with correct results:

This pattern uses tokio::join! to execute multiple concurrent RPC calls, verifying both concurrency and correctness extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:81-96

Error Propagation Testing

Tests verify that server-side errors are correctly propagated to clients with appropriate error codes:

This validates that errors are serialized, transmitted, and deserialized with correct error codes and messages extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:99-152

Large Payload Testing

Tests ensure that payloads exceeding the chunk size are correctly chunked and reassembled:

This pattern validates the streaming chunking mechanism for both requests and responses extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:154-203

Method Not Found Testing

Tests verify that calling unregistered methods returns the correct error code:

This ensures the server correctly identifies missing handlers extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:205-240

Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:81-240 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:126-142

WASM Client Testing with WebSocket Bridge

Testing the WASM client requires special handling because it is runtime-agnostic and designed for browser environments. Integration tests use a WebSocket bridge to connect the WASM client to a real Tokio server:

Bridge Implementation Details

graph TB
    subgraph "Test Environment"
        TEST["Test Function\n#[tokio::test]"]
end
    
    subgraph "Server Side"
        SERVER["RpcServer\nTokio-based"]
LISTENER["TcpListener\n127.0.0.1:random"]
HANDLERS["Registered Handlers\nAdd, Mult, Echo"]
end
    
    subgraph "Bridge Infrastructure"
        WS_CONN["WebSocket Connection\ntokio-tungstenite"]
TO_BRIDGE["mpsc channel\nto_bridge_rx"]
FROM_BRIDGE["ws_receiver\nStreamExt"]
BRIDGE_TX["Bridge Task\nClient→Server"]
BRIDGE_RX["Bridge Task\nServer→Client"]
end
    
    subgraph "WASM Client Side"
        WASM_CLIENT["RpcWasmClient\nRuntime-agnostic"]
DISPATCHER["RpcDispatcher\nblocking_lock()"]
OUTPUT_CB["Output Callback\nsend(bytes)"]
end
    
 
   TEST --> SERVER
 
   TEST --> LISTENER
 
   SERVER --> HANDLERS
    
 
   TEST --> WASM_CLIENT
 
   TEST --> TO_BRIDGE
 
   WASM_CLIENT --> OUTPUT_CB
 
   OUTPUT_CB --> TO_BRIDGE
    
 
   TO_BRIDGE --> BRIDGE_TX
 
   BRIDGE_TX --> WS_CONN
 
   WS_CONN --> SERVER
    
 
   SERVER --> WS_CONN
 
   WS_CONN --> FROM_BRIDGE
 
   FROM_BRIDGE --> BRIDGE_RX
 
   BRIDGE_RX --> DISPATCHER
 
   DISPATCHER --> WASM_CLIENT

The WebSocket bridge consists of two spawned tasks:

Client to Server Bridge : Receives bytes from the WASM client's output callback and forwards them as WebSocket binary messages extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:98-108:

Server to Client Bridge : Receives WebSocket messages and feeds them to the WASM client's dispatcher using spawn_blocking to avoid blocking the async runtime extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:110-123:

Why spawn_blocking : The RpcWasmClient uses synchronous locking (blocking_lock()) and synchronous dispatcher methods because it targets WASM environments where true async is not available. In tests, this synchronous code must run on a blocking thread pool to prevent starving the Tokio runtime.

Sources : extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:1-142 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:83-123

graph LR
    CLIENT_DISP["Client Dispatcher\nRpcDispatcher"]
OUT_BUF["Outgoing Buffer\nRc&lt;RefCell&lt;Vec&lt;u8&gt;&gt;&gt;"]
IN_BUF["Incoming Buffer\nSame as outgoing"]
SERVER_DISP["Server Dispatcher\nRpcDispatcher"]
CLIENT_DISP -->|call request, write_cb| OUT_BUF
 
   OUT_BUF -->|chunks 4| IN_BUF
 
   IN_BUF -->|read_bytes chunk| SERVER_DISP
 
   SERVER_DISP -->|respond response, write_cb| OUT_BUF
 
   OUT_BUF -->|read_bytes| CLIENT_DISP

Unit Testing the RpcDispatcher

The core RpcDispatcher can be tested in isolation without async runtimes or network transports. These tests use in-memory buffers to simulate data exchange:

Test Structure

Unit tests create two RpcDispatcher instances representing client and server, connected via a shared buffer tests/rpc_dispatcher_tests.rs:30-38:

Request Flow : Client creates RpcRequest, calls dispatcher.call() with a write callback that appends to the buffer tests/rpc_dispatcher_tests.rs:42-124:

Server Processing : Server reads from the buffer in chunks, processes requests, and writes responses back tests/rpc_dispatcher_tests.rs:126-203:

This pattern validates framing, chunking, correlation, and protocol correctness without external dependencies.

Sources : tests/rpc_dispatcher_tests.rs:30-203 tests/rpc_dispatcher_tests.rs:1-29

Test Coverage Matrix

The following table summarizes test coverage across different layers and client types:

Test ScenarioCore Unit TestsTokio IntegrationWASM IntegrationCoverage Notes
Basic RPC CallAll layers validated
Concurrent CallsRequires async runtime
Large PayloadsChunking tested at all levels
Error PropagationError serialization validated
Method Not FoundRequires endpoint dispatch
Framing ProtocolImplicitImplicitCore tests focus on this
Request CorrelationImplicitImplicitCore dispatcher tests
WebSocket Transport✓ (bridged)Extension-level tests
Connection StateTransport-specific

Coverage Rationale

  • Core unit tests validate the RpcDispatcher without runtime dependencies
  • Tokio integration tests validate native client-server communication over real WebSocket connections
  • WASM integration tests validate cross-platform compatibility by testing the WASM client against the same server
  • Each layer is tested at the appropriate level of abstraction

Sources : tests/rpc_dispatcher_tests.rs:1-203 extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:1-241 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:1-313

Shared Test Service Definitions

All integration tests use service definitions from example-muxio-rpc-service-definition/src/prebuffered.rs:

Service MethodInput TypeOutput TypePurpose
Add::METHOD_IDVec<f64>f64Sum of numbers
Mult::METHOD_IDVec<f64>f64Product of numbers
Echo::METHOD_IDVec<u8>Vec<u8>Identity function

These methods are intentionally simple to focus tests on protocol correctness rather than business logic. The Echo method is particularly useful for testing large payloads because it returns the exact input, making assertions straightforward.

Method ID Generation : Each method has a unique METHOD_ID generated at compile time by hashing the method name extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs18:

This ensures consistent method identification across all client and server implementations.

Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs1 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs21

Running Tests

Tests are executed using standard Cargo commands:

Test Execution Environment : Most integration tests require a Tokio runtime even when testing the WASM client, because the test infrastructure (server, WebSocket bridge) runs on Tokio. The WASM client itself remains runtime-agnostic.

For detailed information on specific testing approaches, see:

Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs18 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs39

Dismiss

Refresh this wiki

Enter email to refresh