blog/content/posts/2018/07/rust-injection.md

131 lines
4.6 KiB
Markdown

---
title: Rust - controlling side effects from the test.
date: 2018-07-30 00:00:00
tags: [rust, testing, tdd]
series: [rust-testing-tricks]
---
# Rust: controlling side effects from the test.
Hello and welcome to the newest episode on testing in Rust.
Imagine you want to write a timestamping repository of some sorts, that will associate the timestamp of when the storage operation was invoked with the stored value.
How to write it in Rust ? And more importantly - how to test it ?
I would like to share a solution I found and talk a bit about how it works.
Please note that this solution can be used anywhere where you need to pass a handle that is remembered by the production code, and that thing it points to - you then want to change from the test.
```rust
trait Clock {
fn now(&self) -> Instant;
}
struct SystemClock;
impl SystemClock {
fn new() -> Self {
SystemClock {}
}
}
impl Clock for SystemClock {
fn now(&self) -> Instant {
Instant::now()
}
}
struct TimestampingRepository<'a, ClockType>
where
ClockType: Clock + 'a,
{
clock: &'a ClockType,
storage: Vec<(Instant, u32)>, // (timestamp, value)
}
impl<'a, ClockType> TimestampingRepository<'a, ClockType>
where
ClockType: Clock + 'a,
{
fn with_clock(clock: &'a ClockType) -> Self {
TimestampingRepository {
clock,
storage: vec![],
}
}
fn store(&mut self, value: u32) {
self.storage.push((self.clock.now(), value));
}
fn all_stored(&self) -> Vec<(Instant, u32)> {
self.storage.clone()
}
}
#[cfg(test)]
mod should {
#[test]
fn handle_seconds() {
let clock = FakeClock::with_time(Instant::now());
let mut repository = TimestampingRepository::with_clock(&clock);
repository.store(1);
clock.move_by(Duration::from_secs(32));
repository.store(2);
let time_difference = time_difference_between_two_stored(repository);
assert_eq!(32, time_difference.as_secs());
}
struct FakeClock {
now: Instant,
move_by_secs: AtomicUsize,
}
impl FakeClock {
fn with_time(now: Instant) -> Self {
FakeClock {
now,
move_by_secs: AtomicUsize::new(0),
}
}
// WAT no `mut`
fn move_by(&self, duration: Duration) {
self.move_by_secs
.store(duration.as_secs() as usize, Ordering::SeqCst);
}
}
impl Clock for FakeClock {
fn now(&self) -> Instant {
let move_by_secs = self.move_by_secs.load(Ordering::SeqCst) as u64;
self.now + Duration::from_secs(move_by_secs)
}
}
}
```
That's a lot of code. And I already skipped `use`s and some definitions to make it less.
If you want to get the full source code that to follow along - try this [playground](https://play.rust-lang.org/?gist=7f47c441732b543a918cb491487196d4&version=stable&mode=debug&edition=2015) or [this repo](https://github.com/cyplo/rust-dependency-injection) for the full project including production code usage.
Let's start with the test itself.
The clock appears to be immutable (immovable) in the test, yet we call `move_by` on it and the whole thing appears to be working somehow.
First question: can't we just make the clock mutable and skip all this ?
It appears that sadly (but fortunately) Rust prevents us from doing so.
We [cannot](https://doc.rust-lang.org/book/second-edition/ch04-02-references-and-borrowing.html) both have a immutable and mutable borrow of the clock in the same scope.
For the full example with an error go [here](https://play.rust-lang.org/?gist=3e496f857f1e016c596ec7c4060538df&version=stable&mode=debug&edition=2015).
What is this sorcery then ?
We use a type that provides [`Interior Mutability`](https://doc.rust-lang.org/book/second-edition/ch15-05-interior-mutability.html), namely [`AtomicUsize`](https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/std/sync/atomic/struct.AtomicUsize.html).
On the outside - it look immutable, yet it provides a thread-safe and very narrow method of mutating the underlying state.
As we trust `AtomicUsize` to be written correctly, we can then proceed and write our Rust code as usual, relying fully on the borrow checker. Rust compiler is happy and our test code is happy.
I wouldn't use this as a pattern in production code - the borrow checker rules are there for a reason.
Please treat it as an escape hatch to be used in specific situations, situations like this.
Happy Rusting !
p.s. if you'd like to chat about Rust - [ping me an email](mailto:rust@cyplo.dev) !