132 lines
4.6 KiB
Markdown
132 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) !
|