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.

Unit Testing

Relevant source files

Purpose and Scope

This document covers patterns and strategies for unit testing individual components within the rust-muxio system. Unit tests focus on testing isolated functionality without requiring external services, network connections, or complex integration setups. These tests validate core logic such as RpcDispatcher request correlation, service caller interfaces, and custom service method implementations.

For end-to-end testing with real clients and servers communicating over actual transports, see Integration Testing. For error handling patterns in production code, see Error Handling.

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:1-213 tests/rpc_dispatcher_tests.rs:1-204


Unit Testing Architecture

The rust-muxio system is designed to enable comprehensive unit testing through several key architectural decisions:

  1. Runtime-agnostic core : The muxio core library does not depend on async runtimes, allowing synchronous unit tests
  2. Trait-based abstractions : Interfaces like RpcServiceCallerInterface can be easily mocked
  3. Callback-based emission : The RpcDispatcher uses callback functions for output, enabling in-memory testing
  4. Shared service definitions : Method traits enable testing both client and server sides independently

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:20-93 tests/rpc_dispatcher_tests.rs:30-39


Testing the RPC Dispatcher

Synchronous Test Pattern

The RpcDispatcher can be tested synchronously without any async runtime because it operates on byte buffers and callbacks. This enables fast, deterministic unit tests.

Sources: tests/rpc_dispatcher_tests.rs:30-203

graph LR
    subgraph "Client Side"
        ClientDisp["client_dispatcher\n(RpcDispatcher)"]
OutBuf["outgoing_buf\n(Rc<RefCell<Vec<u8>>>)"]
end
    
    subgraph "Server Side"
        ServerDisp["server_dispatcher\n(RpcDispatcher)"]
IncomingBuf["incoming_buf\n(buffer chunks)"]
end
    
    subgraph "Test Flow"
        CallReq["RpcRequest"]
ReadBytes["read_bytes()"]
ProcessReq["Process request"]
Respond["RpcResponse"]
end
    
 
   CallReq -->|client_dispatcher.call| ClientDisp
 
   ClientDisp -->|emit callback| OutBuf
 
   OutBuf -->|chunked read| IncomingBuf
 
   IncomingBuf -->|server_dispatcher| ReadBytes
 
   ReadBytes --> ProcessReq
 
   ProcessReq -->|server_dispatcher.respond| Respond
 
   Respond -->|emit to client| ClientDisp

Key Components in Dispatcher Tests

ComponentTypePurpose
RpcDispatcher::new()ConstructorCreates empty dispatcher instances for client and server
client_dispatcher.call()MethodInitiates RPC request with callback for emitted bytes
server_dispatcher.read_bytes()MethodProcesses incoming bytes and returns request IDs
is_rpc_request_finalized()MethodChecks if request is complete for prebuffered handling
delete_rpc_request()MethodRetrieves and removes complete request from dispatcher
server_dispatcher.respond()MethodSends response back through emit callback

Sources: tests/rpc_dispatcher_tests.rs:37-38 tests/rpc_dispatcher_tests.rs:74-123 tests/rpc_dispatcher_tests.rs:130-198

Example: Testing Request-Response Flow

The dispatcher test demonstrates a complete round-trip with multiple concurrent requests:

Sources: tests/rpc_dispatcher_tests.rs:74-123 tests/rpc_dispatcher_tests.rs:127-202

sequenceDiagram
    participant Test as "Test Code"
    participant ClientDisp as "client_dispatcher"
    participant OutBuf as "outgoing_buf"
    participant ServerDisp as "server_dispatcher"
    participant Handler as "Request Handler"
    
    Test->>ClientDisp: call(ADD_METHOD_ID)
    ClientDisp->>OutBuf: emit bytes
    Test->>ClientDisp: call(MULT_METHOD_ID)
    ClientDisp->>OutBuf: emit bytes
    
    Test->>OutBuf: read chunks
    OutBuf->>ServerDisp: read_bytes(chunk)
    ServerDisp-->>Test: rpc_request_id
    
    Test->>ServerDisp: is_rpc_request_finalized()
    ServerDisp-->>Test: true
    
    Test->>ServerDisp: delete_rpc_request()
    ServerDisp-->>Handler: RpcRequest
    
    Handler->>Handler: decode, compute, encode
    Handler->>ServerDisp: respond(RpcResponse)
    ServerDisp->>ClientDisp: emit response bytes
    
    ClientDisp->>Test: RpcStreamEvent::PayloadChunk

Code Structure for Dispatcher Tests

The test at tests/rpc_dispatcher_tests.rs:30-203 demonstrates the following pattern:

  1. Setup : Create client and server dispatchers with shared buffer (tests/rpc_dispatcher_tests.rs:34-38)
  2. Call Phase : Client dispatcher emits requests to shared buffer (tests/rpc_dispatcher_tests.rs:42-124)
  3. Read Phase : Server dispatcher processes chunked bytes (tests/rpc_dispatcher_tests.rs:127-132)
  4. Handle Phase : Extract finalized requests and process (tests/rpc_dispatcher_tests.rs:134-189)
  5. Respond Phase : Server dispatcher emits response back to client (tests/rpc_dispatcher_tests.rs:192-198)

Sources: tests/rpc_dispatcher_tests.rs:30-203


Testing RPC Service Callers

classDiagram
    class RpcServiceCallerInterface {<<trait>>\n+get_dispatcher() Arc~TokioMutex~RpcDispatcher~~\n+get_emit_fn() Arc~Fn~\n+is_connected() bool\n+call_rpc_streaming() Result\n+set_state_change_handler()}
    
    class MockRpcClient {
        -response_sender_provider SharedResponseSender
        -is_connected_atomic Arc~AtomicBool~
        +get_dispatcher()
        +get_emit_fn()
        +is_connected()
        +call_rpc_streaming()
        +set_state_change_handler()
    }
    
    class TestCase {+test_buffered_call_success()\n+test_buffered_call_remote_error()\n+test_prebuffered_trait_converts_error()}
    
    RpcServiceCallerInterface <|.. MockRpcClient
    TestCase ..> MockRpcClient : uses

Mock Implementation Pattern

Testing client-side RPC logic requires mocking the RpcServiceCallerInterface. The mock implementation provides controlled behavior for test scenarios.

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:24-93

Mock Components

ComponentTypePurpose
MockRpcClientStructTest implementation of RpcServiceCallerInterface
SharedResponseSenderType aliasArc<Mutex<Option<DynamicSender>>> for providing responses
is_connected_atomicFieldArc<AtomicBool> for controlling connection state
DynamicChannelTypeEnumSpecifies bounded or unbounded channel for responses

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:22-28 extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:44-85

Mock Interface Implementation

The MockRpcClient implementation at extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:30-93 shows how to mock each trait method:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:30-93


Testing Success Cases

Prebuffered Call Success Test

The test at extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:97-133 validates successful RPC call flow:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:97-133

Test Structure

  1. Setup mock client : Initialize with sender provider and connection state (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:99-105)
  2. Spawn response task : Background task simulates server sending response (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:110-121)
  3. Create request : Build RpcRequest with method ID and parameters (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:123-128)
  4. Call RPC : Invoke call_rpc_buffered() which awaits response (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs130)
  5. Verify result : Assert response matches expected payload (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs132)

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:97-133


Testing Error Cases

Remote Error Handling Test

The test at extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:135-177 validates error propagation from server to client:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:135-177

Error Types in Tests

The tests validate handling of RpcServiceError variants:

Error TypeCodeTest Scenario
RpcServiceError::Rpc with RpcServiceErrorCode::FailBusiness logic errorItem not found, validation failure (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:155-158)
RpcServiceError::Rpc with RpcServiceErrorCode::SystemSystem errorMethod panic, internal error (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:197-200)

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:155-158 extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:197-200


Testing Trait Methods

RpcMethodPrebuffered Integration

The test at extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:179-212 validates that high-level trait methods correctly propagate errors:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:179-212

Key Test Points

  1. Direct trait method call : Use Echo::call() instead of lower-level APIs (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs203)
  2. Error type preservation : Verify RpcServiceError::Rpc variant is maintained (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:205-210)
  3. Error code and message : Assert both code and message fields are correct (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:207-208)

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:203-211


graph TB
    subgraph "Channel Types"
        Unbounded["DynamicChannelType::Unbounded\n(mpsc::unbounded)"]
Bounded["DynamicChannelType::Bounded\n(mpsc::channel with size)"]
end
    
    subgraph "Channel Components"
        DynSender["DynamicSender\n(enum wrapper)"]
DynReceiver["DynamicReceiver\n(enum wrapper)"]
end
    
    subgraph "Test Usage"
        StoreProvider["Store in\nresponse_sender_provider"]
SpawnTask["Spawned task\npolls for sender"]
SendResponse["send_and_ignore()"]
end
    
 
   Unbounded --> DynSender
 
   Bounded --> DynSender
 
   Unbounded --> DynReceiver
 
   Bounded --> DynReceiver
    
 
   DynSender --> StoreProvider
 
   StoreProvider --> SpawnTask
 
   SpawnTask --> SendResponse
 
   DynReceiver --> StoreProvider

Mock Transport Patterns

Dynamic Channel Management

Tests use dynamic channels to control response timing and simulate various scenarios:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:55-70

Channel Creation in Tests

The call_rpc_streaming() mock implementation at extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:44-85 shows the pattern:

  1. Match channel type : Handle both bounded and unbounded variants (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:55-70)
  2. Create dummy encoder : Return placeholder RpcStreamEncoder (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:72-80)
  3. Store sender : Place sender in shared provider for test task access (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs82)
  4. Return receiver : Client code awaits responses on returned receiver (extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs84)

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:44-85


Test Data Structures

Request and Response Test Fixtures

Unit tests define custom request/response types for validation:

Sources: tests/rpc_dispatcher_tests.rs:7-28 src/rpc/rpc_request_response.rs:9-76

Encoding and Decoding in Tests

The dispatcher tests at tests/rpc_dispatcher_tests.rs:44-49 show how to use bitcode for serialization:

Sources: tests/rpc_dispatcher_tests.rs:44-49 tests/rpc_dispatcher_tests.rs:103-105 tests/rpc_dispatcher_tests.rs:151-167


Common Test Patterns

Pattern: Synchronous In-Memory Testing

For components that don't require async:

Sources: tests/rpc_dispatcher_tests.rs:32-203

Pattern: Async Mock with Controlled Responses

For async components requiring controlled response timing:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:97-133

Pattern: Error Case Validation

For testing error propagation:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:135-177


Summary

The rust-muxio unit testing approach emphasizes:

  1. Isolation : Core components like RpcDispatcher can be tested without async runtimes or network connections
  2. Mock interfaces : Trait-based design enables easy creation of test doubles for RpcServiceCallerInterface
  3. In-memory buffers : Shared Rc<RefCell<Vec<u8>>> buffers simulate network transmission
  4. Dynamic channels : Controlled response delivery via DynamicSender/DynamicReceiver
  5. Error validation : Comprehensive testing of both success and error paths
  6. Type safety : Shared service definitions ensure compile-time correctness even in tests

These patterns enable fast, reliable unit tests that validate component behavior without external dependencies.

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:1-213 tests/rpc_dispatcher_tests.rs:1-204

Dismiss

Refresh this wiki

Enter email to refresh