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

Loading…

Unit Testing

Relevant source files

Purpose and Scope

This document covers unit testing practices and patterns within the muxio codebase. Unit tests verify individual components in isolation using mock implementations and controlled test scenarios. These tests focus on validating the behavior of core RPC components including RpcDispatcher, RpcServiceCallerInterface, and protocol-level request/response handling.

For information about end-to-end testing with real server instances and WebSocket connections, see Integration Testing. For general testing infrastructure overview, see Testing.

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


Unit Testing Architecture

The unit testing strategy in muxio uses isolated component testing with mock implementations to verify behavior without requiring full runtime environments or network transports.

Architecture Overview: Unit tests instantiate mock implementations that satisfy core trait interfaces (RpcServiceCallerInterface) without requiring actual network connections or async runtimes. Mock implementations use synchronization primitives (Arc<Mutex>, Arc<AtomicBool>) to coordinate test behavior and response injection. Tests verify both success paths and error handling using pattern matching against RpcServiceError types.

graph TB
    subgraph "Test Layer"
        TEST["Test Functions\n(#[tokio::test])"]
MOCK["MockRpcClient"]
ASSERTIONS["Test Assertions\nassert_eq!, match patterns"]
end
    
    subgraph "Component Under Test"
        INTERFACE["RpcServiceCallerInterface\ntrait implementation"]
DISPATCHER["RpcDispatcher"]
CALL["call_rpc_buffered\ncall_rpc_streaming"]
end
    
    subgraph "Supporting Test Infrastructure"
        CHANNELS["DynamicSender/Receiver\nmpsc::unbounded, mpsc::channel"]
ENCODER["RpcStreamEncoder\ndummy instances"]
ATOMIC["Arc<AtomicBool>\nconnection state"]
MUTEX["Arc<Mutex<Option<DynamicSender>>>\nresponse coordination"]
end
    
 
   TEST --> MOCK
    MOCK -.implements.-> INTERFACE
 
   MOCK --> CHANNELS
 
   MOCK --> ENCODER
 
   MOCK --> ATOMIC
 
   MOCK --> MUTEX
    
 
   TEST --> CALL
 
   CALL --> DISPATCHER
    
 
   TEST --> ASSERTIONS

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


Mock Implementation Pattern

Unit tests use mock implementations of core interfaces to isolate component behavior. The primary mock pattern implements RpcServiceCallerInterface with controllable behavior.

MockRpcClient Structure

ComponentTypePurpose
response_sender_providerArc<Mutex<Option<DynamicSender>>>Shared reference to response channel sender for injecting test responses
is_connected_atomicArc<AtomicBool>Atomic flag controlling connection state returned by is_connected()
get_dispatcher()Returns Arc<TokioMutex<RpcDispatcher>>Provides fresh dispatcher instance for each test
get_emit_fn()Returns Arc<dyn Fn(Vec<u8>)>No-op emit function (network writes not needed)
call_rpc_streaming()Returns (RpcStreamEncoder, DynamicReceiver)Creates test channels and stores sender for response injection

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

Mock Implementation Details

The MockRpcClient struct implements the RpcServiceCallerInterface trait with minimal functionality required for unit testing:

Key Implementation Characteristics:

graph LR
    subgraph "MockRpcClient Implementation"
        MOCK["MockRpcClient"]
PROVIDER["response_sender_provider\nArc<Mutex<Option<DynamicSender>>>"]
CONNECTED["is_connected_atomic\nArc<AtomicBool>"]
end
    
    subgraph "Trait Methods"
        GET_DISP["get_dispatcher()\nreturns new RpcDispatcher"]
GET_EMIT["get_emit_fn()\nreturns no-op closure"]
IS_CONN["is_connected()\nreads atomic bool"]
CALL_STREAM["call_rpc_streaming()\ncreates channels, stores sender"]
end
    
 
   MOCK --> PROVIDER
 
   MOCK --> CONNECTED
    MOCK -.implements.-> GET_DISP
    MOCK -.implements.-> GET_EMIT
    MOCK -.implements.-> IS_CONN
    MOCK -.implements.-> CALL_STREAM
    
 
   CALL_STREAM --> PROVIDER
 
   IS_CONN --> CONNECTED
  1. Fresh Dispatcher: Each call to get_dispatcher() returns a new RpcDispatcher instance wrapped in Arc<TokioMutex> extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:32-34

  2. No-op Emit: The get_emit_fn() returns a closure that discards bytes, as network transmission is not needed in unit tests extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:36-38

  3. Channel Creation: call_rpc_streaming() creates either bounded or unbounded channels based on DynamicChannelType, stores the sender in shared state for later response injection, and returns a dummy RpcStreamEncoder extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:44-85

  4. Connection State Control: Tests control the is_connected() return value by modifying the AtomicBool extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:40-42

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


Unit Test Structure and Patterns

Prebuffered RPC Call Tests

Tests for prebuffered RPC calls follow a standard pattern: instantiate mock client, spawn response injection task, invoke RPC method, verify result.

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

sequenceDiagram
    participant Test as "Test Function"
    participant Mock as "MockRpcClient"
    participant Task as "tokio::spawn\nresponse task"
    participant Sender as "DynamicSender"
    participant Call as "call_rpc_buffered"
    participant Result as "Test Assertion"
    
    Test->>Mock: Instantiate with Arc<Mutex<Option<DynamicSender>>>
    Test->>Task: spawn background task
    Test->>Call: invoke with RpcRequest
    Call->>Mock: call_rpc_streaming()
    Mock->>Mock: create channels
    Mock->>Sender: store sender in Arc<Mutex>
    Mock-->>Call: return (encoder, receiver)
    Task->>Sender: poll for sender availability
    Task->>Sender: send_and_ignore(Ok(response_bytes))
    Call->>Call: await response from receiver
    Call-->>Test: return (encoder, Result<T>)
    Test->>Result: assert_eq! or match pattern

Test Case: Successful Buffered Call

The test_buffered_call_success function demonstrates the success path for prebuffered RPC calls:

Test Setup:

Response Injection:

Invocation:

Assertion:

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

Test Case: Remote Error Handling

The test_buffered_call_remote_error function verifies error propagation from service handlers:

Error Injection:

Error Verification:

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

Test Case: Trait-Level Error Conversion

The test_prebuffered_trait_converts_error function verifies that the RpcMethodPrebuffered trait correctly propagates service errors:

Trait Invocation:

Error Verification:

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


graph TB
    subgraph "Test Setup"
        TEST["rpc_dispatcher_call_and_echo_response"]
OUTBUF["outgoing_buf\nRc<RefCell<Vec<u8>>>"]
end
    
    subgraph "Client Dispatcher"
        CLIENT_DISP["client_dispatcher\nRpcDispatcher::new()"]
CLIENT_CALL["dispatcher.call()\nwith emit closure"]
CLIENT_STREAM["RpcStreamEvent handler\nreceives responses"]
end
    
    subgraph "Server Dispatcher"
        SERVER_DISP["server_dispatcher\nRpcDispatcher::new()"]
SERVER_READ["dispatcher.read_bytes()\nreturns request IDs"]
SERVER_DELETE["dispatcher.delete_rpc_request()\nretrieves buffered request"]
SERVER_RESPOND["dispatcher.respond()\nemits response frames"]
end
    
    subgraph "Method Handlers"
        ADD_HANDLER["ADD_METHOD_ID\nsum numbers"]
MULT_HANDLER["MULT_METHOD_ID\nmultiply numbers"]
end
    
 
   TEST --> CLIENT_DISP
 
   TEST --> SERVER_DISP
 
   TEST --> OUTBUF
    
 
   CLIENT_CALL --> OUTBUF
 
   OUTBUF --> SERVER_READ
 
   SERVER_READ --> SERVER_DELETE
 
   SERVER_DELETE --> ADD_HANDLER
 
   SERVER_DELETE --> MULT_HANDLER
 
   ADD_HANDLER --> SERVER_RESPOND
 
   MULT_HANDLER --> SERVER_RESPOND
 
   SERVER_RESPOND --> CLIENT_STREAM

RPC Dispatcher Unit Tests

The RpcDispatcher component is tested using a client-server pair of dispatchers that communicate through shared buffers, simulating network transmission without actual I/O.

Test Architecture: Dispatcher Echo Test

Sources: tests/rpc_dispatcher_tests.rs:30-203

Dispatcher Test Flow

The rpc_dispatcher_call_and_echo_response test demonstrates the complete request-response cycle at the dispatcher level:

Phase 1: Request Encoding and Transmission

StepComponentAction
1TestCreates AddRequestParams and MultRequestParams structs tests/rpc_dispatcher_tests.rs:10-28
2TestEncodes parameters using bitcode::encode() tests/rpc_dispatcher_tests.rs:44-46
3TestConstructs RpcRequest with method ID, param bytes, is_finalized: true tests/rpc_dispatcher_tests.rs:42-49
4Client DispatcherCalls dispatcher.call() with chunk size 4, emit closure that writes to outgoing_buf, and stream event handler tests/rpc_dispatcher_tests.rs:74-122
5Emit ClosureExtends outgoing_buf with emitted bytes tests/rpc_dispatcher_tests.rs:80-82

Phase 2: Request Reception and Processing

StepComponentAction
6TestChunks outgoing_buf into 4-byte segments tests/rpc_dispatcher_tests.rs:127-129
7Server DispatcherCalls dispatcher.read_bytes(chunk) returning RPC request IDs tests/rpc_dispatcher_tests.rs:130-132
8Server DispatcherChecks is_rpc_request_finalized() for each request ID tests/rpc_dispatcher_tests.rs:135-142
9Server DispatcherCalls delete_rpc_request() to retrieve and remove finalized request tests/rpc_dispatcher_tests.rs144
10TestDecodes param bytes using bitcode::decode() tests/rpc_dispatcher_tests.rs:152-153

Phase 3: Response Generation and Transmission

StepComponentAction
11Method HandlerProcesses request (sum for ADD, product for MULT) tests/rpc_dispatcher_tests.rs:151-187
12TestEncodes response using bitcode::encode() tests/rpc_dispatcher_tests.rs:157-159
13TestConstructs RpcResponse with rpc_request_id, method ID, status, and payload tests/rpc_dispatcher_tests.rs:161-167
14Server DispatcherCalls dispatcher.respond() with chunk size 4 and emit closure tests/rpc_dispatcher_tests.rs:193-197
15Emit ClosureCalls client_dispatcher.read_bytes() directly tests/rpc_dispatcher_tests.rs195
16Client Stream HandlerReceives RpcStreamEvent::Header and RpcStreamEvent::PayloadChunk tests/rpc_dispatcher_tests.rs:86-118
17Client Stream HandlerDecodes response bytes and logs results tests/rpc_dispatcher_tests.rs:102-112

Sources: tests/rpc_dispatcher_tests.rs:30-203

classDiagram
    class AddRequestParams {+Vec~f64~ numbers}
    
    class MultRequestParams {+Vec~f64~ numbers}
    
    class AddResponseParams {+f64 result}
    
    class MultResponseParams {+f64 result}
    
    AddRequestParams ..|> Encode
    AddRequestParams ..|> Decode
    MultRequestParams ..|> Encode
    MultRequestParams ..|> Decode
    AddResponseParams ..|> Encode
    AddResponseParams ..|> Decode
    MultResponseParams ..|> Encode
    MultResponseParams ..|> Decode

Request and Response Types

The dispatcher test defines custom request and response types that demonstrate the serialization pattern:

Request Types:

All types derive Encode and Decode from the bitcode crate, enabling efficient binary serialization tests/rpc_dispatcher_tests.rs:10-28

Method ID Constants:

Sources: tests/rpc_dispatcher_tests.rs:7-28


Test Coverage Areas

Components Under Test

ComponentTest FileKey Aspects Verified
RpcDispatchertests/rpc_dispatcher_tests.rsRequest correlation, chunked I/O, response routing, finalization detection
RpcServiceCallerInterfaceextensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rsPrebuffered call success, error propagation, trait-level invocation
call_rpc_bufferedextensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rsResponse deserialization, error handling, channel coordination
RpcMethodPrebuffered traitextensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rsTrait method invocation, error conversion

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

Assertion Patterns

Equality Assertions:

Pattern Matching:

match result {
    Err(RpcServiceError::Rpc(err)) => {
        assert_eq!(err.code, RpcServiceErrorCode::Fail);
        assert_eq!(err.message, "item does not exist");
    }
    _ => panic!("Expected a RemoteError"),
}

extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:170-176

Boolean Assertions:

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs:132-211 tests/rpc_dispatcher_tests.rs91


Running Unit Tests

Unit tests use the standard Rust test infrastructure with async support via tokio::test:

Test Attributes:

Running Tests:

Test Organization:

Unit tests are located in two primary locations:

  1. Workspace-level tests: tests/ directory at repository root contains core component tests tests/rpc_dispatcher_tests.rs1
  2. Package-level tests: extensions/<package>/tests/ directories contain extension-specific tests extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs1

Sources: extensions/muxio-rpc-service-caller/tests/prebuffered_caller_tests.rs97 tests/rpc_dispatcher_tests.rs:30-31