Using Fakes for Test-Driven Development in Rust

Introduction to Fakes in TDD with Rust

Welcome to our lesson on using Fakes as test doubles in Test Driven Development (TDD) with Rust. In this lesson, you'll explore how fakes can streamline your testing by simulating real-world components. Our journey so far has exposed you to various test doubles, like dummies, stubs, and mocks. Now, we'll dive into fakes, which enable you to create realistic implementations that mirror complex dependencies, making your tests more robust and reliable. As always, we'll practice the TDD cycle: Red, Green, Refactor, as we see how fakes fit into our testing strategy.

Code Example and Walkthrough: Implementing an In-memory Fake Repository

Let's see how to implement a simple fake: an InMemoryUserRepository. This serves as a stand-in for a real database repository, providing controlled behavior for our tests.

use std::collections::HashMap;
use std::time::SystemTime;

#[derive(Clone, Debug)]
pub struct User {
    pub id: String,
    pub email: String,
    pub name: String,
    pub created_at: SystemTime,
}

pub struct InMemoryUserRepository {
    users: HashMap<String, User>,
    current_id: u32,
}

impl InMemoryUserRepository {
    pub fn new() -> Self {
        InMemoryUserRepository {
            users: HashMap::new(),
            current_id: 1,
        }
    }

    fn generate_id(&mut self) -> String {
        let id = format!("{}", self.current_id);
        self.current_id += 1;
        id
    }

    pub fn create(&mut self, user_data: User) -> User {
        let mut user = user_data.clone();
        user.id = self.generate_id();
        user.created_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();

        self.users.insert(user.id.clone(), user.clone());
        user
    }

    pub fn find_by_id(&self, id: &str) -> Option<&User> {
        self.users.get(id)
    }

    pub fn find_by_email(&self, email: &str) -> Option<&User> {
        self.users.values().find(|&user| user.email == email)
    }

    pub fn update(&mut self, id: &str, data: User) -> Option<User> {
        if let Some(existing) = self.users.get(id) {
            let mut updated = existing.clone();

            if !data.email.is_empty() {
                updated.email = data.email;
            }

            if !data.name.is_empty() {
                updated.name = data.name;
            }

            self.users.insert(updated.id.clone(), updated.clone());
            Some(updated)
        } else {
            None
        }
    }

    pub fn delete(&mut self, id: &str) -> bool {
        self.users.remove(id).is_some()
    }

    pub fn find_all(&self) -> Vec<User> {
        self.users.values().cloned().collect()
    }

    pub fn clear(&mut self) {
        self.users.clear();
        self.current_id = 1;
    }
}

Explanation:

  • We use a Rust HashMap to simulate an in-memory store for users.
  • Each function mimics typical database operations such as creating and finding users.
  • The clear method ensures data isolation between tests — a crucial feature for repeatable outcomes.

By having a controlled data store, we make sure our tests are focused on business logic and not dependent on an external database. Fakes are often quite complicated to build because they mimic the behavior of the real thing. They can be used to verify the state after your code acts on the fake, which can be very useful when you are trying to mimic the environment as best as possible without introducing the uncertainty or delay that the real implementation would introduce.

Building Tests Using the Fake Repository

Next, we will use the fake repository to test a UserService.

  1. Red: Write Failing Tests

Create a test module:

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

    struct UserService<'a> {
        repository: &'a mut InMemoryUserRepository,
    }

    impl<'a> UserService<'a> {
        pub fn register_user(&mut self, email: &str, name: &str) -> User {
            unimplemented!();
        }
    }

    #[test]
    fn test_register_user_should_create_new_user_successfully() {
        // Arrange
        let mut user_repository = InMemoryUserRepository::new();
        let mut user_service = UserService { repository: &mut user_repository };

        // Act
        let user = user_service.register_user("test@example.com", "Test User");

        // Assert
        assert_eq!(user.email, "test@example.com");
        assert_eq!(user.name, "Test User");
        assert!(!user.id.is_empty());
        assert_ne!(user.created_at, 0);

        let users = user_repository.find_all();
        assert_eq!(users.len(), 1);
        assert_eq!(users[0].name, "Test User");
    }
}

Run this test to confirm it fails, as we haven't implemented the logic yet.

  1. Green: Implement Minimal Code

Here's the UserService implementation:

pub struct UserService<'a> {
    repository: &'a mut InMemoryUserRepository,
}

impl<'a> UserService<'a> {
    pub fn register_user(&mut self, email: &str, name: &str) -> User {
        self.repository.create(User {
            id: String::new(),
            email: email.to_string(),
            name: name.to_string(),
            created_at: 0,
        })
    }
}

Rerun the test. It should now pass, confirming our implementation meets the defined requirement.

Review, Summary, and Preparation for Practice Exercises

In this lesson, we explored the implementation and use of fakes in TDD, specifically via an in-memory repository for user management. Remember the steps of TDD:

  • Red: Write a test that fails first, setting clear goals for implementation.
  • Green: Implement just enough code to make your test pass.
  • Refactor: Improve code quality without altering functionality.

Leverage the practice exercises to reinforce these concepts with hands-on examples. Congratulations on navigating the complexities of testing with fakes; your commitment is paving the way for building efficient, scalable applications. This is the final lesson of the course, so kudos for reaching this milestone! Keep exploring and applying TDD principles in your projects.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal