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

Loading…

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 from example-muxio-rpc-service-definition, both clients and servers depend on the same RpcMethodPrebuffered trait implementations. The METHOD_ID constants are generated at compile time via xxhash, ensuring parameter encoding/decoding, method identification, and data structures remain consistent. Tests focus on runtime behavior rather than type mismatches—the compiler prevents protocol incompatibilities.

Layered Testing Mirrors Layered Architecture : The system’s modular design (core muxiomuxio-rpc-service → transport extensions) enables focused testing at each layer. Unit tests in tests/rpc_dispatcher_tests.rs verify RpcDispatcher::call() and RpcDispatcher::respond() behavior without async dependencies, while integration tests in extension crates validate the complete stack including tokio-tungstenite WebSocket transports.

Cross-Platform Validation Is Essential : Because the same RpcMethodPrebuffered traits work across muxio-tokio-rpc-client, muxio-wasm-rpc-client, and muxio-tokio-rpc-server, tests verify that all client types communicate correctly. This is achieved through parallel integration test suites that use identical Add, Mult, and Echo service methods 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 TypeLocationPurposeKey Functions Tested
Core Unit Teststests/rpc_dispatcher_tests.rsValidate RpcDispatcher::call(), RpcDispatcher::respond(), RpcDispatcher::read_bytes() without async runtimerpc_dispatcher_call_and_echo_response()
Tokio Integration Testsextensions/muxio-tokio-rpc-client/tests/Validate RpcClientRpcServer communication over tokio-tungstenitetest_success_client_server_roundtrip(), test_error_client_server_roundtrip()
WASM Integration Testsextensions/muxio-wasm-rpc-client/tests/Validate RpcWasmClientRpcServer with WebSocket bridgetest_success_client_server_roundtrip(), test_large_prebuffered_payload_roundtrip_wasm()
Test Service Definitionsexample-muxio-rpc-service-definition/src/prebuffered.rsShared RpcMethodPrebuffered implementationsAdd::METHOD_ID, Mult::METHOD_ID, Echo::METHOD_ID

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_success_client_server_roundtrip()"
    participant Listener as "TcpListener"
    participant Server as "Arc<RpcServer>"
    participant Endpoint as "RpcServiceEndpointInterface"
    participant Client as "RpcClient"
    participant Add as "Add::call()"
    
    Test->>Listener: TcpListener::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)
    Note over Endpoint: Handler: |request_bytes, _ctx| async move
    Test->>Endpoint: register_prebuffered(Mult::METHOD_ID, handler)
    Test->>Endpoint: register_prebuffered(Echo::METHOD_ID, handler)
    
    Test->>Test: tokio::spawn(server.serve_with_listener(listener))
    
    Test->>Client: RpcClient::new(host, port).await
    
    Test->>Add: Add::call(client.as_ref(), vec![1.0, 2.0, 3.0])
    Add->>Add: Add::encode_request(input)
    Add->>Client: call_rpc_buffered(RpcRequest)
    Client->>Server: WebSocket binary frames
    Server->>Endpoint: Dispatch by Add::METHOD_ID
    Endpoint->>Endpoint: Execute registered handler
    Endpoint->>Client: RpcResponse frames
    Client->>Add: Buffered response bytes
    Add->>Add: Add::decode_response(&bytes)
    Add-->>Test: Ok(6.0)
    
    Test->>Test: assert_eq!(res1.unwrap(), 6.0)

Random Port Binding : Tests use TcpListener::bind("127.0.0.1:0").await 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 instance is wrapped in Arc::new(RpcServer::new(None)) 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 using endpoint.register_prebuffered(Add::METHOD_ID, handler).await, not directly on the server. The endpoint is obtained via server.endpoint(). This separation allows handler registration to complete before server.serve_with_listener() begins accepting connections extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:31-61

Background Server Task : The server runs via tokio::spawn(server.serve_with_listener(listener)), 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 invoke the same Add::call(), Mult::call(), and Echo::call() methods, which internally use Add::encode_request(), Add::decode_response(), etc., ensuring type-safe, consistent serialization via bitcode 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. The pattern uses tokio::join! to execute multiple concurrent calls, verifying both concurrency handling and result correctness:

Each call internally invokes RpcServiceCallerInterface::call_rpc_buffered() with an RpcRequest containing the appropriate METHOD_ID and bitcode-encoded parameters 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 RpcServiceErrorCode values:

The server encodes the error into the RpcResponse.rpc_result_status field, which the client’s RpcDispatcher::read_bytes() method decodes back into a structured RpcServiceError extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:99-152

Large Payload Testing

Tests ensure that payloads exceeding DEFAULT_SERVICE_MAX_CHUNK_SIZE are correctly chunked by RpcDispatcher and reassembled:

The request is automatically chunked in RpcRequest.rpc_prebuffered_payload_bytes via RpcDispatcher::call(), transmitted as multiple frames, buffered by RpcStreamDecoder, and reassembled before the handler executes. The response follows the same chunking path via RpcDispatcher::respond() extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs:154-203 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:230-312

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_success_client_server_roundtrip()"]
end
    
    subgraph "Server Side"
        SERVER["Arc<RpcServer>"]
LISTENER["TcpListener::bind()"]
HANDLERS["endpoint.register_prebuffered()"]
end
    
    subgraph "Bridge Infrastructure"
        WS_CONN["connect_async(server_url)"]
TO_BRIDGE["tokio_mpsc::unbounded_channel()"]
WS_SENDER["ws_sender.send(WsMessage::Binary)"]
WS_RECEIVER["ws_receiver.next()"]
BRIDGE_TX["tokio::spawn(bridge_tx_task)"]
BRIDGE_RX["tokio::spawn(bridge_rx_task)"]
end
    
    subgraph "WASM Client Side"
        WASM_CLIENT["Arc<RpcWasmClient>::new()"]
DISPATCHER["client.get_dispatcher()"]
OUTPUT_CB["Output Callback:\nto_bridge_tx.send(bytes)"]
READ_BYTES["dispatcher.blocking_lock().read_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_SENDER
 
   WS_SENDER --> WS_CONN
 
   WS_CONN --> SERVER
    
 
   SERVER --> WS_CONN
 
   WS_CONN --> WS_RECEIVER
 
   WS_RECEIVER --> BRIDGE_RX
 
   BRIDGE_RX --> READ_BYTES
 
   READ_BYTES --> DISPATCHER
 
   DISPATCHER --> WASM_CLIENT

The WebSocket bridge consists of two spawned tasks that connect RpcWasmClient to the real RpcServer:

Client to Server Bridge : Receives bytes from RpcWasmClient’s output callback (invoked during RpcDispatcher::call()) and forwards them as WsMessage::Binary extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:98-108:

Server to Client Bridge : Receives WsMessage::Binary from the server and feeds them to RpcDispatcher::read_bytes() via task::spawn_blocking() extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:110-123:

Why spawn_blocking : RpcWasmClient::get_dispatcher() returns a type that uses blocking_lock() (a synchronous mutex) and RpcDispatcher::read_bytes() is synchronous. These are required for WASM compatibility where async is unavailable. In tests running on Tokio, synchronous blocking operations must run on the blocking thread pool via task::spawn_blocking() to prevent starving the async 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::new()"]
OUT_BUF["outgoing_buf:\nRc<RefCell<Vec<u8>>>"]
SERVER_DISP["server_dispatcher:\nRpcDispatcher::new()"]
CLIENT_DISP -->|call rpc_request, 4, write_cb| OUT_BUF
 
   OUT_BUF -->|chunks 4| SERVER_DISP
 
   SERVER_DISP -->|read_bytes chunk| SERVER_DISP
 
   SERVER_DISP -->|is_rpc_request_finalized| SERVER_DISP
 
   SERVER_DISP -->|delete_rpc_request| SERVER_DISP
 
   SERVER_DISP -->|respond rpc_response, 4, write_cb| OUT_BUF
 
   OUT_BUF -->|client.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

The rpc_dispatcher_call_and_echo_response() test creates two RpcDispatcher instances representing client and server, connected via a shared Rc<RefCell<Vec<u8>>> buffer tests/rpc_dispatcher_tests.rs:30-38:

Request Flow : Client creates RpcRequest with rpc_method_id set to ADD_METHOD_ID or MULT_METHOD_ID, then invokes RpcDispatcher::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 4-byte chunks via RpcDispatcher::read_bytes(), checks is_rpc_request_finalized(), retrieves the request with delete_rpc_request(), processes it, and sends the response via RpcDispatcher::respond() tests/rpc_dispatcher_tests.rs:126-203:

This validates the complete request/response cycle including framing, chunking, request correlation via rpc_request_id, and method dispatch via rpc_method_id.

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 IntegrationKey Functions Validated
Basic RPC CallRpcDispatcher::call(), RpcDispatcher::respond()
Concurrent Callstokio::join! with multiple RpcCallPrebuffered::call()
Large Payloads (> DEFAULT_SERVICE_MAX_CHUNK_SIZE)RpcStreamEncoder::write(), RpcStreamDecoder::process_chunk()
Error PropagationRpcServiceError::Rpc, RpcServiceErrorCode
Method Not FoundRpcServiceEndpointInterface::dispatch()
Framing ProtocolImplicitImplicitRpcDispatcher::read_bytes() chunking
Request CorrelationImplicitImplicitrpc_request_id in RpcHeader
WebSocket Transport✓ (bridged)tokio-tungstenite, WsMessage::Binary
Connection Stateclient.handle_connect(), client.handle_disconnect()

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 TypeImplementationPurpose
Add::METHOD_IDVec<f64>f64request_params.iter().sum()Sum of numbers
Mult::METHOD_IDVec<f64>f64request_params.iter().product()Product of numbers
Echo::METHOD_IDVec<u8>Vec<u8>Identity functionRound-trip validation

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

Method ID Generation : Each method has a unique METHOD_ID constant generated at compile time by xxhash::xxh3_64 hashing of the method name. This is defined in the RpcMethodPrebuffered trait implementation:

The Add::METHOD_ID, Mult::METHOD_ID, and Echo::METHOD_ID constants are used both in test code (Add::call()) and in server handler registration (endpoint.register_prebuffered(Add::METHOD_ID, handler)), ensuring consistent method identification across all implementations extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs18

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