Mocking struct members w/o changing the API

Yesterday I took some time to catch up on testing tasks until I struck some (for me) complicated design choices. As a result, I spent the better half of the day pondering on how to structure my code for better testability.

General consensus regarding testing/architecture appears to be to either use traits and dynamic dispatch or, for the compile time equivalent generics. I understand that these approaches typically have the nice side effect of loosening tight coupling. However, I found both approaches not always ideal for my case. In particular, they introduce a notion of extendability even if there is no need for it (with some pesky consequences). This situation really leaves me dissatisfied.

The following example shows my problem with this approach:

mod foreign {
    type ValueList = Vec<u8>;
    pub struct SomeClient {}
    impl SomeClient {
        pub fn get() -> ValueList { vec![] }
        pub fn put(_elem: u8) { }
    }
}

trait SomeTrait {}

mod local {
    use super::foreign;

    struct MyForeignModule {
        client: foreign::SomeClient,
    }
    
    impl MyForeignModule {
        pub fn try_create() -> Result<MyForeignModule, ()> {
            Ok(MyForeignModule {
                client: foreign::SomeClient {},
            })
        }
    }
    
    impl super::SomeTrait for MyForeignModule {}
}

pub fn main() {
}

Here MyForeignModule is the module that I want to write tests for. This module makes use an external library foreign. I know that I don't want to swap/configure for another client type in the future.

Making MyForeignModule generic over the client type would imply a major change in the API and make the client type configurable. Furthermore, the generic parameters will then surface everywhere I use my module.

Using dynamic dispatch instead would needlessly introduce additional overheads (one could argue the severity, but still).

So toying around with generics + trait bounds I came to this solution:

mod foreign {
    pub type ValueList = Vec<u8>;

    pub struct SomeClient {}
    impl SomeClient {
        pub fn get(&self) -> ValueList {
            vec![]
        }
        pub fn put(&self, _elem: u8) {}
    }
}

trait SomeTrait {}

mod local {
    use super::foreign;

    mod detail {
        use super::*;
    
        pub trait Foreign {
            fn get(&self) -> foreign::ValueList;
            fn put(&self, elem: u8);
        }

        impl Foreign for foreign::SomeClient {
            fn get(&self) -> foreign::ValueList {
                foreign::SomeClient::get(self)
            }

            fn put(&self, elem: u8) {
                foreign::SomeClient::put(self, elem)
            }
        }

        pub trait Creatable<C>
        where
            C: Foreign,
        {
            fn create_helper() -> Result<InnerMyForeignModule<C>, ()>;
        }

        impl Creatable<foreign::SomeClient> for InnerMyForeignModule<foreign::SomeClient> {
            fn create_helper() -> Result<InnerMyForeignModule<foreign::SomeClient>, ()> {
                Ok(InnerMyForeignModule {
                    client: foreign::SomeClient {},
                })
            }
        }

        pub struct InnerMyForeignModule<C: Foreign> {
            client: C,
        }

        impl<C: Foreign> InnerMyForeignModule<C>
        where
            InnerMyForeignModule<C>: Creatable<C>,
        {
            pub fn try_create() -> Result<InnerMyForeignModule<C>, ()> {
                Ok(InnerMyForeignModule::create_helper()?)
            }
        }

        impl<C: Foreign> crate::SomeTrait for InnerMyForeignModule<C> {}
    }

    // public type as before
    pub type MyForeignModule = detail::InnerMyForeignModule<foreign::SomeClient>;

    // mock type to use for testing purposes
    struct MockClient {}

    impl detail::Foreign for MockClient {
        fn get(&self) -> foreign::ValueList {
            vec![]
        }

        fn put(&self, _elem: u8) {}
    }

    type TestForeignModule = detail::InnerMyForeignModule<MockClient>;
}

pub fn main() {}
type or paste code here

Here I split the module into an internal and external implementation and make InnerMyForeignModule generic over the client type. Externally, I reexports that internal implementation then for a public interface. I found that this actually requires some boilerplate code and I would be interested in some opinions regarding this solution and the problem in general :).

To be frank I don't think this is true as-is, and I don't see the value in it given this specific scenario. If you want to test a concrete type, why don't you just test that concrete type?

Creating a trait just for the sake of creating a trait is likely an instance of the Concrete Abstraction or Faux Abstraction anti-pattern.

The example is very boiled down; in my case I have some external backend library that does not provide any proper means to mock it. Think for example of a database. Generally one should abstract the library as much away as possible, however, there is some database specific boilerplate code involved that I want to be able to test.

So what I thought of was some sort of PIMPL pattern using generics.

Indeed, the additional traits here are rather a means to an end (and to satisfy the compiler :P) and I'm not 100% happy myself (maintenance is getting worse through more code).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.