asomers / mockall

A powerful mock object library for Rust
Apache License 2.0
1.45k stars 61 forks source link

Mocking an internal struct with external dependency #543

Closed guvenir18 closed 8 months ago

guvenir18 commented 8 months ago

Hello. First of all, thank you for your work on this great crate. I am still learning Rust and I would really appreciate any help I can get.

I have to mock an internal struct, lets say Service_A which is like:

pub struct Service_A {
    session: Ext_Service,
}

Where Ext_Service is another struct from external crate I have no control over. I am trying to mock this like that:

mod mocks {

   use::Ext_Crate::Ext_Service;

    pub struct Service_A {
        session: Ext_Service,
    }

    mockall::mock! {
        pub Service A {
            session: Ext_Service,
        }

        // From Service_A.rs file
        impl Service_A {
              // Methods
        }
        // From another file
        impl Service_A {
              // Methods
        }

    }
}

Then use it like that:

#[cfg(not(test))]
use crate::Services::Service_A;

#[cfg(test)]
use mocks::Service_A;

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

    pub fn test() {
        let service_a = Service_A {};
       // Other parts of test that will use mock Service_A
    }

}

My question Is, do I have to mock Ext_Service to mock Service_A ?. I was thinking about maybe I can pass some kind of NULL value instead of Ext_Service but it is not possible in Rust without wrapping field in Option<> as far as I know. Also, I am not sure about if my mocking is correct because I get an error from macro which says "message: impl block must implement a trait" . Thanks for help

guvenir18 commented 8 months ago

I have solved this issue, but now I have another problem. Lets say I am writing test for a struct UnderTest and this struct requires Service_A which we have mocked. Problem is when I run cargo test, it compiles my whole project and somewhere in other modules, a struct AnotherStruct expects Service_A, not MockService_A but I want MockService_A used only for current module where source code and unit tests of UnderTest are.

asomers commented 8 months ago

Even though you say you've solved your first problem, I'll give the solution here for the benefit of future readers:

Mock objects have no members. You can't even declare them, which was the source of your error message. Instead, mock objects only have methods and traits.

As for the second problem, you just need to be more careful with your use statements. Try putting the appropriate use statements (without or without mockall_double::double, as appropriate) in each module.

guvenir18 commented 8 months ago

Yes, I did your solution.

About second one, I did not understand how I should handle use statements. I thought cargo would compile each test case separately so if I do

#[cfg(not(test))]
use crate::Services::Service_A;

#[cfg(test)]
use mocks::MockService_A as Service_A;

pub struct UnderTest {
        service: Service A,
}

on a source file, I thought it will only change Service_A with MockService_A on current file and other parts of the code would be unaffected but cargo compile whole project when I run cargo test and since UnderTest uses MockService_A, code pieces that use Service_A throught UnderTest gives "expected Service_A, got MockService_A" compilation error. An example would be on anıother file which uses UnderTest through use crate:

let new_service = UnderTest::new(service); -> gives compilation error since this part normally expects Service_A but gets mock one. What I want is MockService_A will only be used for this test module and nowhere else.

I dont know how to handle this situation. What would be best practice here. I would really appreciate your help

asomers commented 8 months ago

When you build unit tests, cargo compiles your whole lib or bin into a single executable. And Mockall doesn't do anything magical with type replacement. It just relies on those #[cfg(...)] use statements to replace original types with mock types at compile time. Those use statements each have module scope, so you simply need to ensure that you use the correct ones in each module.

When using mocks, the best practice is to cleanly layer your code. For each layer but the lowest, write unit tests that use a mock version of the lower layer. It should look something like this:

mod highest {
    #[double]
    use crate::lowest::Bar;

    struct Foo {
        bar: Bar   // in unit tests, this will really be MockBar
    }
    #[automock]
    impl Foo {...}

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

        #[test]
        fn my_test() {
            let mut mock = Bar::default();    // this is really MockBar
            let foo = Foo::new();
            ...
        }
    }
}

mod middle {
    #[double]
    use crate::lowest::Baz;

    struct Bar{
        baz: Baz   // in unit tests, this will really be MockBaz
    }
    #[automock]
    impl Bar {...}

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

        #[test]
        fn my_test() {
            let mut mock = Baz::default();    // this is really MockBaz
            let bar = Bar::new();
            ...
        }
    }
}

mod lowest {
    struct Baz {}
    #[automock]
    impl Baz {...}
}
guvenir18 commented 8 months ago

Your example clarified everything, I made it work. One last question. I configured my code so it uses mocks in test but thing is I also have integration tests which are labeled with #[cfg(test)] so when compiler tries to build integration tests, it fails. How should I handle this situation ? I was thinking setting a feature in Cargo.toml but it may really compilicate cfg statements.

guvenir18 commented 8 months ago

I have solved this issue to. Previously, I put my integration tests under src directory but this was wrong since they have to be on same level with src folder so cargo can treat them as an external crate. Thanks for your help, you can close this issue.

asomers commented 8 months ago

Glad to help.