Unlock 100% Coverage—Mock Your Rust Unit Tests the Right Way
The essential mocking pattern every Rustacean should keep in their toolbox.
Writing unit tests for pure, in-memory logic is easy—you feed it data, check the result, and you’re done. The trouble starts when your code has to reach outside itself: a database, an HTTP API, the Bitcoin network, or any third-party crate. Those dependencies can be slow or a pain to set up, so tests fail for the wrong reasons or need a mess of setup scripts. If you’re like me, this is when I skip the tests—and my coverage stays stuck under 70%.
The classic fix? Mock those dependencies.
In this post, you’ll learn how to mock external calls in Rust—and how to do it effectively.
First, let’s see why this problem shows up with a concrete example.
The Need for Mocking
Suppose you’re building an e-commerce backend: before restocking an item, you need to pay your supplier in Bitcoin.
Let's say the BitcoinWallet type comes from an external crate you can't edit:
pub struct BitcoinWallet;
impl BitcoinWallet {
pub fn send(&mut self, to: &str, amount: f64) -> Result<Txid> {
/* broadcasts a transaction */
}
}
(Notice the send function that reaches out to the network)
Your business logic call the send function from the BitcoinWallet like this:
pub fn fill_inventory(wallet: &mut BitcoinWallet, item_id: u64, qty: u32) -> anyhow::Result<()> {
let cost_btc = calculate_cost(item_id, qty); // calculate_cost is defined elsewhere
wallet.send(SUPPLIER_ADDR, cost_btc)?; // interacts with network
Ok(())
}
And here’s a test:
#[test]
fn test_fill_inventory() {
let mut wallet = BitcoinWallet::new();
let result = fill_inventory(&mut wallet, 1000, 5);
assert!(result.is_ok());
}
But here’s the catch:
If you run this test, it might take forever — waiting for network confirmations, or it might fail because your test wallet is empty, or the testnet node is down—even though your logic is solid.
What you want is to simulate this call so your tests stay fast and reliable.
In this post, our mission is to test the fill_inventory function without a real Bitcoin wallet.
Mocking in Rust
Why can’t we just swap the wallet for a dummy object, like in some other languages?
Rust doesn’t let you swap out objects at runtime (no reflection).
Types and lifetimes are fixed at compile time.
The solution: Introduce a trait — so you can inject either the real wallet or a mock as needed.
The Indirection Layer
Here’s the trick:
Instead of talking directly to the external wallet, your code will use a trait. Then you implement that trait for both the real wallet (for production) and your mock (for testing).
Is there other approach?
You might see other patterns in the wild:
Passing closures or function pointers (okay for very simple cases).
Compiling in different modules for tests with feature flags or `cfg(test)`.
HTTP record/replay tools (good for web APIs, but not for Bitcoin).
In real-world Rust, traits are the most flexible and reliable way to swap dependencies for testing.
How Do We Mock BitcoinWallet? (Step by Step)
For this demo, the code for BitcoinWallet is simplified.
Define the Trait and Use It for our code
pub trait BtcWallet {
fn send(&mut self, address: &str, amount_btc: f64) -> Result<Txid>;
}
Note that we use the exact signature as the send function from the real BitcoinWallet type.
Next update your business logic to use the trait:
pub const SUPPLIER_ADDR: &str = "bc1q-supplier-addr";
pub fn fill_inventory<W: BtcWallet>(
wallet: &mut W,
item_id: u64,
qty: u32,
) -> anyhow::Result<()> {
let cost_btc = calculate_cost(item_id, qty);
wallet.send(SUPPLIER_ADDR, cost_btc)?;
update_inventory_db(item_id, qty)?;
Ok(())
}
Now, let’s extend our original BitcoinWallet so it implements our trait.
Implement the Trait for the Real Wallet
use external_crate::BitcoinWallet as RealBitcoinWallet;
use anyhow::Context;
impl BtcWallet for RealBitcoinWallet {
fn send(&mut self, address: &str, amount_btc: f64) -> Result<Txid> {
RealBitcoinWallet::send(self, address, amount_btc)
}
}
Look at this line: RealBitcoinWallet::send(self, address, amount_btc), we specifically call send function defined on the RealBitcoinWallet type.
Now everything works just like before—let’s create our mock wallet for testing.
Our Mock Object for Testing
#[derive(Default)]
pub struct MockBitcoinWallet {
pub called: bool,
pub last_addr: String,
pub last_amt: f64,
}
impl BtcWallet for MockBitcoinWallet {
fn send(&mut self, address: &str, amount_btc: f64) -> Result<Txid> {
self.called = true;
self.last_addr = address.to_owned();
self.last_amt = amount_btc;
Ok("mock-tx-hash".into())
}
}
Because we use &mut self, we can track calls using simple struct fields (bool, String, f64). If we don't have a &mut self, a simple trick is to use iterior mutability types, such as: AtomicBool and Mutex.
And finally, our test can run with the mock:
#[test]
fn test_fill_inventory_calls_send() {
let mut wallet = MockBitcoinWallet::default();
fill_inventory(&mut wallet, 42, 3).unwrap();
let expected = calculate_cost(42, 3);
assert!(wallet.called);
assert_eq!(wallet.last_addr, SUPPLIER_ADDR);
assert!((wallet.last_amt - expected).abs() < 1e-8);
}
Pro-tip: You can check not only that a method was called, with what arguments, but you can also return different outputs to verify if your code handles cases correctly, e.g: return an error if amount is negative.
If writing these mocks by hand feels tedious, there’s a better way.
Using Mockall for Less Boilerplate
As your codebase grows, you might want to avoid writing mocks by hand. Try mockall.
First, add this to your Cargo.toml:
[dev-dependencies]
mockall = "0.13.1"
Here’s how you use it:
#[cfg_attr(test, mockall::automock)]
pub trait BtcWallet {
fn send(&mut self, address: &str, amount_btc: f64) -> Result<Txid>;
}
#[test]
fn test_fill_inventory() {
let mut mock = MockBtcWallet::new();
let expected = calculate_cost(42, 3);
mock.expect_send()
.withf(|addr, amt| addr == SUPPLIER_ADDR && (*amt - expected).abs() < 1e-8)
.times(1);
fill_inventory(&mut mock, 42, 3).unwrap();
}
How does this work?
Notice the line #[cfg_attr(test, mockall::automock)]. This tells Rust: “When running in the test environment, use the mockall::automock macro to generate a mock implementation for this trait.” The macro creates a new mock type — MockBtcWallet — that implements the BtcWallet trait for you. With just this one line, you get a fully functional mock (with all the trait methods) ready for your tests.
automock is just a useful macro that mockall support to quickly create a mock type. There are plenty of useful things it exposes. Please checkout the documentation here if you need more powerful features.
When Should You Mock?
You might be wondering: Should I mock everything? The answer: mock the pain, not the world.
Use mocks for unit tests, especially when external calls would slow down or break your tests.
For end-to-end QA before release, use regtest/testnet or the real wallet to catch integration bugs.
For pure calculation functions, there’s no need to mock — just test them directly.
If you need to check the real wallet RPC protocol, always test against the actual service at least once.
Pro-tip: If your tests are slow or flaky because of network calls, use mocks. But always keep at least one integration test that hits the real thing, just in case.
Summary
Mocks let you test code that talks to the outside world — quickly and safely.
In Rust, a trait layer lets you swap between the real wallet and a test mock.
Hand-written mock is only for your understanding; use mockall to save time.
Always keep a few real integration tests to catch surprises.
If you enjoyed this post or found it helpful, I'm Cuong, and I regularly write about Rust and other interesting programming topics. I'd love to connect with you—feel free to reach out on X, LinkedIn, or subscribe to my blog for more insights!