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 :).