How to use dynamic dispatch for a trait having generic function

I am trying to create an app with dependency injection pattern so that all DB related functions can be mocked in unit testing.
So I have defined a trait with the DB function. Then define a trait object type which I will be passing to my handler function. At the time of testing I will be passing a mock object to the handler function.
Now the problem is that the DB function find_one inside the trait is a generic function. So I am getting compilation error like this trait cannot be made into an object.
Is there any workaround for this scenario?
The find_one function in my trait MongoClient has to be generic because in the implementation of this trait function I will call collection function from mongodb crate which is a generic function.

use std::sync::Arc;

use async_trait::async_trait;
use mongodb::{
    bson::{doc, Document},
    error::Result as MongoResult,
    options::{ClientOptions, FindOneOptions},
    Client,
};
use serde::de::DeserializeOwned;

#[cfg(test)]
use mockall::automock;

// define a trait for Mongo Client that can be used for dependency injection
#[cfg_attr(test, automock)]
#[async_trait]
trait MongoClient {
    async fn find_one<T>(
        &self,
        db: &str,
        coll: &str,
        filter: Option<Document>,
        options: Option<FindOneOptions>,
    ) -> MongoResult<Option<T>>
    where
        T: DeserializeOwned + Unpin + Send + Sync;
}

// define trait object type
type DbObject = Arc<dyn MongoClient + Send + Sync + 'static>;

struct AppDatabase(Client);

#[async_trait]
impl AppDatabase {
    async fn new() -> Self {
        let uri = "http://localhost:27017/mydb";
        let client_options = ClientOptions::parse(uri).await.unwrap();
        let client = Client::with_options(client_options).unwrap();
        Self(client)
    }
}

#[async_trait]
impl MongoClient for AppDatabase {
    async fn find_one<T>(
        &self,
        db: &str,
        coll: &str,
        filter: Option<Document>,
        options: Option<FindOneOptions>,
    ) -> MongoResult<Option<T>> {
        let collection = self.0.database(db).collection::<T>(coll);
        collection.find_one(filter, options).await
    }
}

#[tokio::main]
async fn main() {
    println!("Unit testing with Mockall");
    let db = AppDatabase::new().await;
    let db_object = Arc::new(db);
    handle_req(db_object).await;
}

async fn handle_req(db_object: DbObject) {
    let filter = doc! {};
    let result = db_object
        .find_one::<Document>("myDB", "myColl", filter, None)
        .await
        .unwrap();
    println!("{:?}", result);
}

Does it have to be a trait object? AFAICT from your example and your statement

the only reason you want to have a trait is for mocking it for your unit tests? I never used mockall, but they have a section that says you can mock structs as well as traits: mockall - Rust. Maybe you could mock AppDatabase directly, without having to create a trait object to begin with?

I'm asking, because making your trait object safe would require you to remove the generic parameter from the function, greatly impeding your interface.

If you do need it to be a trait object, you can sometimes get away with "hoisting" the type parameter up on to the trait

use std::sync::Arc;

use async_trait::async_trait;
use mongodb::{
    bson::{doc, Document},
    error::Result as MongoResult,
    options::{ClientOptions, FindOneOptions},
    Client,
};
use serde::{de::DeserializeOwned, Deserialize};

#[cfg(test)]
use mockall::automock;

// define a trait for Mongo Client that can be used for dependency injection
#[cfg_attr(test, automock)]
#[async_trait]
trait MongoClient<T: 'static> {
    async fn find_one(
        &self,
        db: &str,
        coll: &str,
        filter: Option<Document>,
        options: Option<FindOneOptions>,
    ) -> MongoResult<Option<T>>
    where
        T: DeserializeOwned + Unpin + Send + Sync;
}

/// An type that can be pulled from the database other than `Document`
#[derive(Deserialize)]
struct Example {}

/// Trait objects can't include more than one non-auto trait, so we create an empty trait here which includes both instantiations of `MongoClient` as super traits.
trait DynMongoClient: MongoClient<Document> + MongoClient<Example> {}

impl<T> DynMongoClient for T where T: MongoClient<Document> + MongoClient<Example> {}

// define trait object type
type DbObject = Arc<dyn DynMongoClient + Send + Sync + 'static>;

struct AppDatabase(Client);

impl AppDatabase {
    async fn new() -> Self {
        let uri = "http://localhost:27017/mydb";
        let client_options = ClientOptions::parse(uri).await.unwrap();
        let client = Client::with_options(client_options).unwrap();
        Self(client)
    }
}

#[async_trait]
impl<T> MongoClient<T> for AppDatabase
where
    T: DeserializeOwned + Unpin + Send + Sync + 'static,
{
    async fn find_one(
        &self,
        db: &str,
        coll: &str,
        filter: Option<Document>,
        options: Option<FindOneOptions>,
    ) -> MongoResult<Option<T>> {
        let collection = self.0.database(db).collection::<T>(coll);
        collection.find_one(filter, options).await
    }
}

#[tokio::main]
async fn main() {
    println!("Unit testing with Mockall");
    let db = AppDatabase::new().await;
    let db_object = Arc::new(db);
    handle_req(db_object).await;
}

async fn handle_req(db_object: DbObject) {
    let filter = doc! {};
    // Have to use UFCS syntax to specify to the type parameter
    let result =
        MongoClient::<Document>::find_one(&*db_object, "myDB", "myColl", Some(filter), None)
            .await
            .unwrap();
    println!("{:?}", result);
}

You end up needing to specify all of the types that the trait can deserialize on the trait object, which isn't ideal. But sometimes it's the simplest option when you really need a trait object.

2 Likes

You are right. It does not have to be trait object. Actually I am writing a web app with axum and mongodb crates. All I want the ability to unit test my handler functions without actually making database connections. I saw the example codes and picked up the dependency injection pattern from there. But I guess in this case it would be wise not to use the trait object pattern. It would much easier to mock the struct AppDatabase. There is also another crate mockall_double which makes it easier to mock the structs. I have tried out an example code here.

1 Like

Actually I am thinking something like this to achieve. Thanks for showing me with example. But I think in this case I will not use the trait object. Using trait object seems unnecessary complication. Maybe this trick will come handy sometime in future.

1 Like

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.