admin管理员组

文章数量:1127954

Say I have two traits

traits.rs

pub trait MessagingService {
    fn new() -> Self
...
}

pub trait WebService {
    fn create_client() -> Client;
    fn send_request(req: RequestBuilder) -> Option<Value>;
}

and a module where I implement these two traits for discord

services/discord.rs

pub struct DiscordService {
    client: Client,
}

impl WebService for DiscordService {
// a.k.a the transport layer
   fn send_request(req: RequestBuilder) -> Option<Value> {
       // contains all the code involved that involves actual HTTP requests
   }
}

impl MessagingService for DiscordService {
// contains the code that
// i) build request
// ii) processing of the values returned by WebService::send_request
    fn update_task_status(&self, task: &mut Task) {
       let body = json!({"what":"ever"});
       let _ = Self::send_request(
            self.client
                .post(format!(
                    "{BASE_URL}/channels/{}/messages",
                    CONF.discord_channel
                ))
                .json(&body),
        );
    }
}

I want to mock the transport part, i.e. WebService so I could test the functions of MessagingService independently.

I read on several occasions that the proper rustacean way to proceed is to provide a mock for the trait instead of mocking the object. I understood this as providing a new implementation of this very trait in my tests.

services/discord.rs

...
#[cfg(test)]
mod tests {
    use reqwest::blocking::{Client, RequestBuilder};
    use serde_json::Value;
    use super::DiscordService;
    use crate::structs::WebService;

    impl WebService for DiscordService {
        fn send_request(req: RequestBuilder) -> Option<Value>{
            // builds Values from sourced files instead 
            // of sending HTTP requests 
        }
    }
    #[test]
    fn some_test() {
    let notifier: DiscordService = DiscordService::new();
    // test notifier.update_task_status
    }
}

It looked nice on paper but I get a conflicting implementations of trait WebService for type discord::DiscordService. I thought that mod tests {} would work like an encapsulation, allowing me to provide an alternative implementation of WebService but it does not seem to be the case. I do not want to create a new struct like DiscordServiceMock, for it would make some parts of my code untestable.

Is there a way to provide such implementation that would no end up in conflict? Is there an other way?

Say I have two traits

traits.rs

pub trait MessagingService {
    fn new() -> Self
...
}

pub trait WebService {
    fn create_client() -> Client;
    fn send_request(req: RequestBuilder) -> Option<Value>;
}

and a module where I implement these two traits for discord

services/discord.rs

pub struct DiscordService {
    client: Client,
}

impl WebService for DiscordService {
// a.k.a the transport layer
   fn send_request(req: RequestBuilder) -> Option<Value> {
       // contains all the code involved that involves actual HTTP requests
   }
}

impl MessagingService for DiscordService {
// contains the code that
// i) build request
// ii) processing of the values returned by WebService::send_request
    fn update_task_status(&self, task: &mut Task) {
       let body = json!({"what":"ever"});
       let _ = Self::send_request(
            self.client
                .post(format!(
                    "{BASE_URL}/channels/{}/messages",
                    CONF.discord_channel
                ))
                .json(&body),
        );
    }
}

I want to mock the transport part, i.e. WebService so I could test the functions of MessagingService independently.

I read on several occasions that the proper rustacean way to proceed is to provide a mock for the trait instead of mocking the object. I understood this as providing a new implementation of this very trait in my tests.

services/discord.rs

...
#[cfg(test)]
mod tests {
    use reqwest::blocking::{Client, RequestBuilder};
    use serde_json::Value;
    use super::DiscordService;
    use crate::structs::WebService;

    impl WebService for DiscordService {
        fn send_request(req: RequestBuilder) -> Option<Value>{
            // builds Values from sourced files instead 
            // of sending HTTP requests 
        }
    }
    #[test]
    fn some_test() {
    let notifier: DiscordService = DiscordService::new();
    // test notifier.update_task_status
    }
}

It looked nice on paper but I get a conflicting implementations of trait WebService for type discord::DiscordService. I thought that mod tests {} would work like an encapsulation, allowing me to provide an alternative implementation of WebService but it does not seem to be the case. I do not want to create a new struct like DiscordServiceMock, for it would make some parts of my code untestable.

Is there a way to provide such implementation that would no end up in conflict? Is there an other way?

Share Improve this question asked Jan 8 at 17:18 zar3bskizar3bski 3,1517 gold badges30 silver badges68 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

No, this is not how Rust works or should be used. Multiple, overlapping implementations for a type are not allowed, as stated by the Rust Reference:

A trait implementation is considered incoherent if either the orphan rules check fails or there are overlapping implementation instances.

There are several solutions to this. None of them are Rust-specific.

If you stick to the idea of mocking a part of your code, you can treat a trait as an interface and use some kind of strategy pattern (which involves injecting different strategies). Then implement a test strategy (like DiscordServiceMock) and use it in your tests.

Here is a small example of how to use WebService as a strategy for DiscordService:

use std::marker::PhantomData;

trait WebService {
    fn send_request() -> i32;
}

trait MessagingService {
    fn update_task_status(&self) -> i32;
}

#[derive(Default)]
struct DiscordService<T>(PhantomData<T>);

impl<T: WebService> MessagingService for DiscordService<T> {
    fn update_task_status(&self) -> i32 {
        T::send_request()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Default)]
    struct WebServiceMock;

    impl WebService for WebServiceMock {
        fn send_request() -> i32 {
            100
        }
    }

    #[test]
    fn test_name() {
        let service = DiscordService::<WebServiceMock>::default();
        assert_eq!(service.update_task_status(), 100);
    }
}

本文标签: rustHow to provide a mock for a trait (no mockall magics)Stack Overflow