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
- extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs
- extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs
- src/rpc/rpc_internals/rpc_header.rs
- src/rpc/rpc_request_response.rs
- tests/rpc_dispatcher_tests.rs
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 muxio → muxio-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 Type | Location | Purpose | Key Functions Tested |
|---|---|---|---|
| Core Unit Tests | tests/rpc_dispatcher_tests.rs | Validate RpcDispatcher::call(), RpcDispatcher::respond(), RpcDispatcher::read_bytes() without async runtime | rpc_dispatcher_call_and_echo_response() |
| Tokio Integration Tests | extensions/muxio-tokio-rpc-client/tests/ | Validate RpcClient → RpcServer communication over tokio-tungstenite | test_success_client_server_roundtrip(), test_error_client_server_roundtrip() |
| WASM Integration Tests | extensions/muxio-wasm-rpc-client/tests/ | Validate RpcWasmClient → RpcServer with WebSocket bridge | test_success_client_server_roundtrip(), test_large_prebuffered_payload_roundtrip_wasm() |
| Test Service Definitions | example-muxio-rpc-service-definition/src/prebuffered.rs | Shared RpcMethodPrebuffered implementations | Add::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 Scenario | Core Unit Tests | Tokio Integration | WASM Integration | Key Functions Validated |
|---|---|---|---|---|
| Basic RPC Call | ✓ | ✓ | ✓ | RpcDispatcher::call(), RpcDispatcher::respond() |
| Concurrent Calls | ✗ | ✓ | ✓ | tokio::join! with multiple RpcCallPrebuffered::call() |
Large Payloads (> DEFAULT_SERVICE_MAX_CHUNK_SIZE) | ✓ | ✓ | ✓ | RpcStreamEncoder::write(), RpcStreamDecoder::process_chunk() |
| Error Propagation | ✓ | ✓ | ✓ | RpcServiceError::Rpc, RpcServiceErrorCode |
| Method Not Found | ✗ | ✓ | ✓ | RpcServiceEndpointInterface::dispatch() |
| Framing Protocol | ✓ | Implicit | Implicit | RpcDispatcher::read_bytes() chunking |
| Request Correlation | ✓ | Implicit | Implicit | rpc_request_id in RpcHeader |
| WebSocket Transport | ✗ | ✓ | ✓ (bridged) | tokio-tungstenite, WsMessage::Binary |
| Connection State | ✗ | ✓ | ✓ | client.handle_connect(), client.handle_disconnect() |
Coverage Rationale
- Core unit tests validate the
RpcDispatcherwithout 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 Method | Input Type | Output Type | Implementation | Purpose |
|---|---|---|---|---|
Add::METHOD_ID | Vec<f64> | f64 | request_params.iter().sum() | Sum of numbers |
Mult::METHOD_ID | Vec<f64> | f64 | request_params.iter().product() | Product of numbers |
Echo::METHOD_ID | Vec<u8> | Vec<u8> | Identity function | Round-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:
- Unit Testing - Patterns for testing individual components
- Integration Testing - End-to-end testing with real transports
Sources : extensions/muxio-tokio-rpc-client/tests/prebuffered_integration_tests.rs18 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs39