How to test something that wraps a boxed trait object?


#1

I’ve a system that has to build a transport stack. The layers in the stack are generic at runtime, so I have to box a trait. The current structure looks as follows, with TTransport a trait that offers byte-level operations.

/// Implementation of the binary protocol.
pub struct TBinaryProtocol {
    /// Set to `true` if the strict binary protocol is to be used.
    pub strict: bool,

    /// Underlying transport used for byte-level operations.
    pub transport: Rc<RefCell<Box<TTransport>>>,
}

Here’s the problem though: at runtime I don’t need to know the exact TTransport type because the stack doesn’t care what layers compose it. But during testing I want to supply a test ``TTransportthat has additional methods I can use to inspect and set internal read/write buffers. Unfortunately this isn't possible here because exact type wrapped byTProtocolis lost when I doBox` and so I no longer have access to those methods. What’s the best practice for a situation like this?


#2

You might be able to accomplish what you’re describing with the Any trait. It offers a way to downcast to concrete types.

However, you might be better off just testing the observable behavior; this would have the effect of reducing coupling of your unit tests and the actual implementation. Furthermore, it would make your tests easier to maintain.


#3

The external behavior is (are?) the bytes written to the wrapped transport. Unfortunately the TTransport trait does not contain the methods required to investigate those bytes because usually you don’t have to. (Imagine that TTransprot is a TSocketTransport; you won’t ever ask it for its read/write buffer.) But it a test circumstance I would, to verify that my protocol is writing out the correct bytes.


#4

Can your TTransport test implementation wrap like a Rc<Vec<u8>> and your test could just hold onto a second reference to that?

Edit: Would need some sort of internal mutability for this route so maybe Rc<RefCell<Vec<u8>>> internally or Arc<Mutex<Vec<u8>>> if this is shared across threads.


#5

Hi,

so I would suggest the following, using the Any approach suggested above.

trait DebuggableTransport {
    //your debug fns
}

impl DebuggableTransport for MyTransport {
    // impls
}

fn test() {
    let proto = TBinaryProtocol { strict: false, transport: Rc::new(RefCall::new(Box::new(MyTransport::new()))) };
    proto.write(/* something */);
    let inner_transport = proto.transport.downcast_ref::<MyTransport>().unwrap();
    /// call your debug methods
}

So in TTransport doesn’t really matter here, it’s just that the structure in use also implements another trait and you can get the structure back out here.

One question: what’s the reason for TBinaryProtocol using dynamic dispatch and boxing the transport instead of being generic in itself?

Best,
Florian


#6

Thanks @skade - I’ll give this approach a try.

I need dynamic dispatch and boxing because the choice of transport is generic at runtime. Basically, the application can determine (based on configuration) which combination of transport and protocol to use to communicate with a remote. The simplest way to demonstrate/implement this is:

let t = match transport_type { ... }
let p = match &protocol_type {
  "binary" => TBinaryProtocol(t),
  "compact" => TCompactProtocol(t),
  _ => // error...
}

#7

I considered that. Every CS problem can be solved by an additional layer of indirection and all :slight_smile:


#8

@skade The approach you’ve suggested cannot work because (as I just noticed) you’re creating Box<TransportType> in your example as opposed to Box<TTransport>. Unfortunately because my types are generic at runtime I can’t use that approach and instead, have to use trait objects.

I think I’m going to have to create an additional layer of indirection and just use that.

Basically:


   protocol.inner
         |
         V
  wrapper_transport (boxed trait object)
         |
         V
  actual_test_transport

Super annoying.