Mocks and Fixtures in Rust: How to Test Code Without Pain

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Tests in Rust Are Not Boring



If you've ever written tests for code in Python or JavaScript, you've certainly encountered a situation: your method calls an external API, reads a file from disk, or accesses a database. Running such tests in a real environment is a pain. They are slow, unreliable (what if the API goes down?) and require environment setup.



In Rust, the situation is no different. Only Rust is a systems language, and here tests must be fast, reliable, and deterministic. That's exactly what mock objects and fixtures are for.



In this article, we'll look at how to write tests in Rust that don't depend on the outside world. You'll learn how to substitute dependencies, prepare data for tests, and do it elegantly. Let's go!



What are Mock Objects and Fixtures?



Mock Objects

A mock object is an imitation of a real system component. You create an object that behaves the way you need for the test: returns specified values, throws errors, or verifies that its methods were called with the correct arguments.



Imagine you have an email sending service. In a test, you don't want to actually send emails — you use a mock that simply checks that the service was called with the correct address and text.



Fixtures

Fixtures are prepared data or state needed to run a test. For example, a test database, configuration file, or preset structures. In Rust, fixtures are often implemented through regular functions that return ready-made objects.



Together, mocks and fixtures give you full control over the test environment. You test only your logic, not the operation of external services.



Basic Principles of Testing in Rust



Before diving into mocks, let's recall how tests are structured in Rust. A typical unit test looks like this:



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

#[test] fn test_addition() { assert_eq!(2 + 2, 4); }}


It's simple. But when your code starts depending on external resources (files, HTTP requests, databases), problems arise. The solution is to invert dependencies and use the Dependency Injection pattern.



Practical Example: Data Loading Service



Let's say we have a DataLoader struct that loads data from an external API and processes it. We want to test the processing without calling the real API.



First, let's define a trait (interface) for the data source:



pub trait DataSource {    fn fetch_data(&self) -> Result<String, String>;}


Now let's implement a real source that makes an HTTP request (simplified):



pub struct RealDataSource {    pub url: String,}

impl DataSource for RealDataSource { fn fetch_data(&self) -> Result<String, String> { // Here would be a real HTTP request Ok(format!("Data from {}", self.url)) }}


And a service that uses this source:



pub struct DataLoader<T: DataSource> {    source: T,}

impl<T: DataSource> DataLoader<T> { pub fn new(source: T) -> Self { DataLoader { source } }

pub fn load_and_process(&self) -> Result<String, String> { let raw_data = self.source.fetch_data()?; // Some processing Ok(format!("Processed: {}", raw_data)) }}


Note: DataLoader does not depend on a specific implementation of DataSource. It works with any type that implements this trait. This is the key to testability.



Creating a Mock Object Manually



The simplest way is to create a test struct

Blogs

Book Recommendations