Skip to content

Instantly share code, notes, and snippets.

@timabell
Last active May 3, 2023 10:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timabell/8813d851399908987396c1725aa8b6d6 to your computer and use it in GitHub Desktop.
Save timabell/8813d851399908987396c1725aa8b6d6 to your computer and use it in GitHub Desktop.
GPT tells me all the ways of doing DI in Rust
gpt ID
q6aKyg2zki0Jt4oexqSc6ZK171VeDzuS

https://twitter.com/tim_abell/status/1651286661509136386?s=19

You

how do you inject dependencies for testing in rust

Genie

In Rust, one way to inject dependencies for testing is through the use of traits and trait objects. The idea is to define a trait for each dependency that your code depends on, and then provide mock implementations of these traits for testing purposes. Here's an example:

// Define a trait for the dependency
trait MyDependency {
    fn some_method(&self) -> i32;
}

// Your code depends on a type that implements the MyDependency trait
struct MyType<T: MyDependency> {
    dep: T,
}

impl<T: MyDependency> MyType<T> {
    fn new(dep: T) -> Self {
        Self { dep }
    }

    fn do_something(&self) -> i32 {
        self.dep.some_method()
    }
}

// Mock implementation of the dependency for testing purposes
struct MockDependency;

impl MyDependency for MockDependency {
    fn some_method(&self) -> i32 {
        42
    }
}

// Unit test
#[test]
fn my_type_works() {
    let my_type = MyType::new(MockDependency);
    assert_eq!(my_type.do_something(), 42);
}

In this example, we define a MyDependency trait that represents the dependency that our code depends on. We then define MyType struct which has a generic type parameter T that must implement MyDependency trait.

We can now write a mock implementation of MyDependency, called MockDependency, which we can use in our unit tests.

The key point here is that we can create a MyType instance with the MockDependency instance injected as a dependency, allowing us to test the behavior of MyType without actually using the real MyDependency.

This approach allows you to keep your production code decoupled from specific implementations of dependencies, so that it can be easily tested and maintained.

You

how else could this be done?

Genie

Another way to inject dependencies for testing in Rust is by using a dependency injection (DI) crate, such as rust-inject.

With rust-inject, you can use a Container to register your dependencies and then inject those dependencies into your code at runtime. The Container can also hold mock implementations of those dependencies for testing purposes.

Here's an example:

use rust_inject::Container;

// Define a trait for the dependency
trait MyDependency {
    fn some_method(&self) -> i32;
}

// Implementation of the dependency
struct MyRealDependency;

impl MyDependency for MyRealDependency {
    fn some_method(&self) -> i32 {
        42
    }
}

// Your code depends on a type that implements the MyDependency trait
struct MyType<T: MyDependency> {
    dep: T,
}

impl<T: MyDependency> MyType<T> {
    fn new(dep: T) -> Self {
        Self { dep }
    }

    fn do_something(&self) -> i32 {
        self.dep.some_method()
    }
}

// Unit test
#[test]
fn my_type_works() {
    // Create a Container and register a mock implementation of the dependency
    let mut container = Container::new();
    container.register::<dyn MyDependency, _>(Box::new(MockDependency));

    // Resolve MyType instance from the container with the mock dependency
    let my_type = container.resolve::<MyType<dyn MyDependency>>();
    assert_eq!(my_type.do_something(), 42);
}

In this example, we define a MyRealDependency struct which implements MyDependency trait. We then define MyType struct which has a generic type parameter T that must implement MyDependency trait.

We can now write a mock implementation of MyDependency, called MockDependency, which we can register with a Container instance provided by the rust-inject crate.

The key point here is that we create a MyType instance from the Container with the registered mock implementation of MyDependency, allowing us to test the behavior of MyType without actually using the real MyDependency.

This approach may be more suitable for large-scale applications where multiple dependencies need to be injected and managed, as it provides a more formal way of handling dependency injection.

You

are there other ways?

Genie

Yes, there are other ways to inject dependencies for testing in Rust. Another option is to use a mocking framework like mockers.

With mockers, you can create mock objects that mimic the behavior of the real objects and then use them to test your code. This approach can be particularly useful when you need to test complex interactions between objects.

Here's an example:

use mockers::Scenario;
use mockers_derive::mocked;

// Define a trait for the dependency
trait MyDependency {
    fn some_method(&self) -> i32;
}

// Implementation of the production dependency
struct MyRealDependency;

impl MyDependency for MyRealDependency {
    fn some_method(&self) -> i32 {
        42
    }
}

// Your code depends on a type that implements the MyDependency trait
struct MyType<T: MyDependency> {
    dep: T,
}

impl<T: MyDependency> MyType<T> {
    fn new(dep: T) -> Self {
        Self { dep }
    }

    fn do_something(&self) -> i32 {
        self.dep.some_method()
    }
}

// Mock implementation of the dependency
#[mocked]
trait MockDependency: MyDependency {}

#[test]
fn my_type_works() {
    // Create a scenario for mocking
    let scenario = Scenario::new();

    // Create a mock object for the dependency
    let mock_dep = scenario.create_mock::<MockDependency>();

    // Tell the mock object how to behave
    scenario.expect(mock_dep.some_method_call().and_return(42));

    // Create a MyType instance with the mock dependency
    let my_type = MyType::new(mock_dep);

    // Call a method on MyType and assert the result
    assert_eq!(my_type.do_something(), 42);
}

In this example, we define a MyRealDependency struct which implements MyDependency trait. We then define MyType struct which has a generic type parameter T that must implement MyDependency trait.

We can now use the mockers crate to create a mock implementation of MyDependency, called MockDependency. This creates an object that behaves like the real MyDependency but allows us to specify its behavior during testing.

We then create a Scenario and add an expectation that the some_method method of mock_dep should be called and return 42. Then we create a MyType instance with the mock dependency and call its do_something method. Finally, we assert that the result is 42.

This approach may be more suitable for applications where most of the dependencies are already pre-built and only require slight modification to handle edge cases or testing purposes.

You

any other ways?

Genie

Yes, another way to inject dependencies for testing in Rust is to use the Arc<RwLock<dyn Trait>> pattern. This approach allows you to create a shared reference-counted pointer to the dependency that can be safely shared across multiple threads.

Here's an example:

use std::sync::{Arc, RwLock};

// Define a trait for the dependency
trait MyDependency {
    fn some_method(&self) -> i32;
}

// Implementation of the production dependency
struct MyRealDependency;

impl MyDependency for MyRealDependency {
    fn some_method(&self) -> i32 {
        42
    }
}

// Your code depends on a type that implements the MyDependency trait
struct MyType<T: MyDependency> {
    dep: Arc<RwLock<T>>,
}

impl<T: MyDependency> MyType<T> {
    fn new(dep: T) -> Self {
        Self { dep: Arc::new(RwLock::new(dep)) }
    }

    fn do_something(&self) -> i32 {
        let guard = self.dep.read().unwrap();
        guard.some_method()
    }
}

#[test]
fn my_type_works() {
    // Create a MyRealDependency instance
    let real_dep = MyRealDependency;

    // Wrap the real dependency in an Arc and create a MyType instance with it
    let arc_dep = Arc::new(RwLock::new(real_dep));
    let my_type = MyType::new(arc_dep.clone());

    // Call a method on MyType and assert the result
    assert_eq!(my_type.do_something(), 42);
}

In this example, we define a MyRealDependency struct which implements MyDependency trait. We then define MyType struct which has a generic type parameter T that must implement MyDependency trait.

We can now create a shared reference-counted pointer to MyRealDependency using Arc<RwLock<MyRealDependency>>. We then wrap the shared reference-counted pointer in another Arc<RwLock<dyn MyDependency>> and pass it to MyType constructor.

We call some_method() on the guard obtained from read-locking the RwLock in do_something() and assert that its value is equal to 42.

This approach is helpful when you need to share resources such as DB connections, network connections, or files across multiple threads. It provides an efficient way to handle concurrency while testing your code.

You

what about with conditional compilation

Genie

Conditional compilation in Rust allows you to compile different parts of your code depending on certain conditions. You can use conditionals to control whether or not certain blocks of code are included in the final binary.

When it comes to dependency injection, conditional compilation can be used to inject different dependencies into your code depending on whether you're building for production or testing.

Here's an example:

trait MyDependency {
    fn some_method(&self) -> i32;
}

// Conditional compilation for production
#[cfg(not(test))]
struct MyRealDependency;

#[cfg(not(test))]
impl MyDependency for MyRealDependency {
    fn some_method(&self) -> i32 {
        42
    }
}

// Conditional compilation for testing
#[cfg(test)]
struct MyMockDependency;

#[cfg(test)]
impl MyDependency for MyMockDependency {
    fn some_method(&self) -> i32 {
        24
    }
}

// Your code depends on a type that implements the MyDependency trait
struct MyType<T: MyDependency> {
    dep: T,
}

impl<T: MyDependency> MyType<T> {
    fn new(dep: T) -> Self {
        Self { dep }
    }

    fn do_something(&self) -> i32 {
        self.dep.some_method()
    }
}

#[test]
fn my_type_works() {
    // Create a MyMockDependency instance and pass it to MyType
    let mock_dep = MyMockDependency;
    let my_type = MyType::new(mock_dep);

    // Call a method on MyType and assert the result
    assert_eq!(my_type.do_something(), 24);
}

fn main() {
    // Create a MyRealDependency instance and pass it to MyType
    let real_dep = MyRealDependency;
    let my_type = MyType::new(real_dep);

    // Call a method on MyType and print the result
    let result = my_type.do_something();
    println!("{}", result);
}

In this example, we define a MyDependency trait for the dependency. We then use conditional compilation to define different implementations of MyDependency depending on whether or not we're building for testing.

If we're building for production (#[cfg(not(test))]), we define MyRealDependency which implements MyDependency and returns 42 from some_method(). If we're building for testing (#[cfg(test)]), we define MyMockDependency which also implements MyDependency but returns 24 from some_method().

We then define MyType struct which depends on a type that implements MyDependency trait. We use generics to ensure that the dependency can be either MyRealDependency or MyMockDependency depending on the configuration.

We create instances of MyType in the main() function and in the my_type_works() test function and call the same method on both instances. Depending on the configuration, the calls will return different values.

Conditional compilation is a powerful feature in Rust that allows you to control which parts of your code are included in the final binary based on certain conditions such as the target platform or the build configuration. This makes it easy to inject different dependencies into your code for testing or production purposes.

You

show dependeny injection for tests with rust macros

Genie

Rust macros can be used to write dependency injection code for tests. In Rust, we can use macro_rules! macro to define simple declarative macros or we can use the proc_macro crate to implement more powerful procedural macros.

Here's an example of a simple declarative macro that uses dependency injection for testing:

trait MyDependency {
    fn some_method(&self) -> i32;
}

struct MyRealDependency;

impl MyDependency for MyRealDependency {
    fn some_method(&self) -> i32 {
        42
    }
}

// Define a macro that injects a test dependency
macro_rules! inject_test_dependency {
    () => {
        struct MyMockDependency;

        impl MyDependency for MyMockDependency {
            fn some_method(&self) -> i32 {
                24
            }
        }

        let _ = MyMockDependency;
    };
}

// Our code depends on a type that implements the MyDependency trait
struct MyType<T: MyDependency> {
    dep: T,
}

impl<T: MyDependency> MyType<T> {
    fn new(dep: T) -> Self {
        Self { dep }
    }

    fn do_something(&self) -> i32 {
        self.dep.some_method()
    }
}

#[test]
fn my_type_works() {
    // Inject the test dependency using the inject_test_dependency! macro
    inject_test_dependency!();

    // Create a MyMockDependency instance and pass it to MyType
    let mock_dep = MyMockDependency;
    let my_type = MyType::new(mock_dep);

    // Call a method on MyType and assert the result
    assert_eq!(my_type.do_something(), 24);
}

fn main() {
    // Create a MyRealDependency instance and pass it to MyType
    let real_dep = MyRealDependency;
    let my_type = MyType::new(real_dep);

    // Call a method on MyType and print the result
    let result = my_type.do_something();
    println!("{}", result);
}

In this example, we define a MyDependency trait for the dependency. We then define MyRealDependency which implements MyDependency and returns 42 from some_method().

We then define a macro called inject_test_dependency! which defines a test implementation of MyDependency called MyMockDependency. The macro creates an instance of MyMockDependency to ensure that it is included in the compiled binary.

We then define MyType struct which depends on a type that implements MyDependency trait. We use generics to make sure that the dependency can be either MyRealDependency or MyMockDependency.

In the my_type_works() test function, we use the inject_test_dependency! macro to inject the test dependency and create an instance of MyMockDependency which we pass to MyType. We call a method on MyType and assert that the result is 24, which is the value returned by the test implementation of MyDependency.

Note that macros can be powerful tools in Rust because they allow you to generate code at compile-time. However, they can also be tricky to use properly so be sure to consult the Rust documentation before attempting to write your own macros.

You

list all the ways you know of to do dependency injection for tests in rust

Genie

There are a few ways to do dependency injection for tests in Rust. Here are some of the most common ways:

  1. Constructor Injection: Pass in the dependencies as parameters to the constructor of the object being tested. This technique uses the power of Rust's generics and traits to make it possible to inject different implementations of the same trait.

    Example:

    trait MyDependency {
        fn some_method(&self) -> i32;
    }
    
    struct MyRealDependency;
    
    impl MyDependency for MyRealDependency {
        fn some_method(&self) -> i32 {
            42
        }
    }
    
    // Our code depends on a type that implements the MyDependency trait
    struct MyType<T: MyDependency> {
        dep: T,
    }
    
    impl<T: MyDependency> MyType<T> {
        fn new(dep: T) -> Self {
            Self { dep }
        }
    
        fn do_something(&self) -> i32 {
            self.dep.some_method()
        }
    }
    
    #[test]
    fn my_type_works() {
        // Create a MyRealDependency instance and pass it to MyType
        let real_dep = MyRealDependency;
        let my_type = MyType::new(real_dep);
    
        // Call a method on MyType and assert the result
        assert_eq!(my_type.do_something(), 42);
    }
  2. Trait Object Injection: Pass in the dependencies as trait objects (i.e., a reference or a pointer to a trait). This technique allows you to change the implementation of the trait at runtime, which can be useful for testing.

    Example:

    trait MyDependency {
        fn some_method(&self) -> i32;
    }
    
    struct MyRealDependency;
    
    impl MyDependency for MyRealDependency {
        fn some_method(&self) -> i32 {
            42
        }
    }
    
    // Our code depends on a trait object that implements the MyDependency trait
    struct MyType {
        dep: Box<dyn MyDependency>,
    }
    
    impl MyType {
        fn new(dep: Box<dyn MyDependency>) -> Self {
            Self { dep }
        }
    
        fn do_something(&self) -> i32 {
            self.dep.some_method()
        }
    }
    
    #[test]
    fn my_type_works() {
        // Create a MyRealDependency instance and pass it to MyType as a trait object
        let real_dep: Box<dyn MyDependency> = Box::new(MyRealDependency);
        let my_type = MyType::new(real_dep);
    
        // Call a method on MyType and assert the result
        assert_eq!(my_type.do_something(), 42);
    }
  3. Global Variable Injection: Define a global variable that holds the dependency, and reference it from the test. This technique is simple but can be dangerous because the global state can affect the behavior of other tests.

    Example:

    trait MyDependency {
        fn some_method(&self) -> i32;
    }
    
    struct MyRealDependency;
    
    impl MyDependency for MyRealDependency {
        fn some_method(&self) -> i32 {
            42
        }
    }
    
    // Define a global variable holding the dependency
    static DEP: MyRealDependency = MyRealDependency;
    
    // Our code depends on a type that uses the global variable
    struct MyType;
    
    impl MyType {
        fn do_something(&self) -> i32 {
            DEP.some_method()
        }
    }
    
    #[test]
    fn my_type_works() {
        // Call a method on MyType and assert the result
        assert_eq!(MyType.do_something(), 42);
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment