A Tale of Testability and Sending Non-Send Types in Rust
This is a story of testability, multithreading, and the age old question of
how do we send a !Send
type in Rust. I’ll explore how (not) to do this,
while rambling on about how writing obsessively testable code leads to better design.
Hot takes incoming.
Testability and Multithreading
Let’s take a few steps back and set the scene: say we have a function that spawns a thread. We’ll keep it simple –trivial even– to focus on the essentials. Let’s say that all this thread does, is play some audio1. For a first try, we could structure our code like so:
struct SystemAudio {/* ...*/}
impl SystemAudio {
fn new(/* config */) -> Self {
/*...*/
}
fn play_music(&self) {
/* ...*/
}
}
// ❓ how do we test this??
fn spawn_thread(audio: SystemAudio) {
std::thread::spawn(move || {
audio.play_music();
});
}
To my mind, there’s a problem with this code and it stems from the fact that
I am a huge nut for testability. I need to make sure that I can test the
behavior of that function. The least I want to do, is make sure that the
play_music
function of the audio
instance really gets called.
One great way to make all kinds of code testable is dependency inversion,
which boils down to coding against interfaces rather than concrete types2. So
rather than passing in our SystemAudio
instance directly, we’ll
define a trait Audio
to abstract over the behavior of the audio backend.
This allows us to mock the audio backend for testing. During testing, we can
pass in our mock to make sure that the correct behavior was indeed invoked. So
let’s refactor our code:
trait Audio {
fn play_music(&self);
}
struct SystemAudio {/* ...*/}
impl SystemAudio {
fn new(/* config */) -> Self {
/*...*/
}
}
impl Audio for SystemAudio {
fn play_music(&self) {
/* ...*/
}
}
// ⚡ this doesn't compile
fn spawn_thread<A>(audio: A)
where A: Audio {
std::thread::spawn(move || {
audio.play_music();
});
}
You probably already knew this wouldn’t compile. The compiler will correctly
complain that A
is neither Send
nor 'static
, which is what we’d need
to move it into the thread. If we can further restrict A
to be Send
and
'static
, that’s a fine solution and that would be the end of this article.
But what if we can’t? What if our production audio backend really does not
implement Send
?
How Do We impl Send for a !Send Type?

If we’ve written a type T
, where the compiler doesn’t automatically implement
Send
but we know it would be sound to do so, we can of course implement Send
manually and call it a day3. However, what if the compiler doesn’t implement
Send
on our type for a good reason? Let’s say we’re using a field of foreign type
that is itself not Send
. We’d better assume that the crate authors deliberately did not implement
Send
on that type. Thus, just overriding their decision by manually implementing Send
on our
type might be unsound. So that’s out of the question, unless we have a very good reason
to believe it’s sound.
Using Mutex<T>
and Arc<Mutex<T>>
Why don’t we just wrap it in a Mutex<T>
? Or an Arc<Mutex<T>>
for good measure?
That usually helps with all kinds of multithreading-induced compile errors, right?
The unfortunate truth is that Mutex<T>
is Send
if and only if
T
is Send
, so sticking a non-Send
type into a mutex won’t make it Send
.
The same
is true for Arc<T>
and thus for Arc<Mutex<T>>
, so that won’t help either4.
Interlude: Isn’t There a Crate for That?
The short answer is: No, I don’t think so, let’s have a look on crates.io:
mutex-extra
: aims to create aSend
type from a non-Send
type. Diplays a “my code is erroneous, don’t use” warning. I guess we won’t be using that then.send_cells
: a pretty recent crate claiming to be an alternative tofragile
(see below).sendable
: I believe this crate is concerned with sharing resources, rather than moving values across threads. We might be able to use it for our purpose, but it would only give us runtime guarantees that we didn’t do anything wrong.send-cell
: deprecated in favor offragile
.fragile
: “wrap a value and provide aSend
bound. Neither of the types permit access to the enclosed value unless the thread that wrapped the value is attempting to access it”. That’s the opposite of what we want.
Please tell me if I’m wrong about this: for fundamental reasons, I can’t imagine a
crate existing that gives us Send
wrappers for non-Send
types without using
unsafe
code that would be equivalent to implementing Send
ourselves. We’ve
already ruled that out for good reason.
Why Even Test spawn_thread
?
Yes, why even do that if the Rust compiler makes it so hard? Even if we generally agree on the value of testing, we might be tempted to refactor our code like so:
fn spawn_thread() {
std::thread::spawn(|| {
let audio = SystemAudio::new(/*config*/);
execute_thread(audio);
});
}
fn execute_thread<A: Audio>(audio: A) {
audio.play_music();
/* and all other logic...*/
}
Now, we can write a nice test for execute_thread
, since that still depends
on an interface, rather than a concrete type. Isn’t this basically just as
good as testing spawn_thread
? After all, spawn_tread
only calls
execute_thread
with a SystemAudio
instance, which we want to use in
production anyways. Call me crazy, but I’ll argue this is not good enough.
To my mind, we should treat items under test as black boxes as much as is
feasible. Testing execute_thread
as a substitute for spawn_thread
relies
on the knowledge that spawn_thread
only calls execute_thread
in a new
thread. But from a testing point of view, that’s an implementation detail we
shouldn’t care about. We should instead be interested in making sure that
spawn_thread
does the right thing. This might seem overly pedantic, but
imagine someone else5 editing our spawn_thread
and inadvertendly introducing a bug.
I personally would want to have a test for spawn_thread
that flags if something
unexpected happens, to catch errors higher up the chain.
For me, that’s the most important thing. It’s not about getting a couple lines more test coverage,
but it’s about testing the behavior of the things that are actually used… at
all levels of integration6. I’ll one up myself and also
claim that putting in the effort to make spawn_thread
testable leads to a better design.
Advanced Dependency Inversion
Don’t get me wrong, if it’s possible to restrict our type to be Send + 'static
,
we should definitely do that and move it into the thread, like we initially
intended. We just have to come to terms with the fact that we simply can’t
do that in this scenario. To overcome this, we can take inspiration from
the previous section. What we did there, was to create the audio
instance
in the thread itself, rather than move it into the thread from the outside.
Let’s expand on this idea: rather than hardcode the creation of the audio
instance in the thread itself, we create an API to inject some code to create
the instance for us. This sounds more complicated than it is, and
there are many ways to do that7. I like this one:
fn spawn_thread<F,A>(audio_constructor: F)
where F: FnOnce()-> A + Send + 'static,
A: Audio {
std::thread::spawn(move || {
let audio = audio_constructor();
audio.play_music();
});
}
Instead of passing in the audio
instance itself, we now pass in a callable
that constructs the instance. The callable F
itself is restricted on Send + 'static
,
but A
is not restricted on either of these bounds. I’ve created a slighty
more involved example on the playground
that illustrates the use. Here’s what we can do now:
// passing a default system backend
// using a closure.
spawn_thread(||SystemAudio::default());
// a mocking backend
spawn_thread(||MockAudio);
// passing in a configuration
let config = AudioConfiguration {device: 123, volume: 0.5};
spawn_thread(move ||SystemAudio::with_config(config));
Using the new API like this is almost as simple as passing in
the instance itself, and it makes spawn_thread
completely testable.
We just have to add ||
or move ||
. One problem we have to deal with
is constructor failure, meaning the constructor function might
return a Result<A,E>
rather than an A
directly. We have to make the
thread communicate the error to the outside. However, that problem isn’t unique
to this approach, so I’ll leave it at that.
Why It’s Better
In this last section, let me defend my claim that this design is better,
not only because it makes spawn_thread
testable. This design also decouples
the implementation of spawn_thread
from the actual audio backend again, by
using dependency inversion. This means that we can use different audio backends
either at runtime (by refactoring to dyn Audio
), or at compile time e.g. for
different operating systems or hardware.
I believe the benefits of this approach completely justify the additional complexity…
even if this starts looking a bit like a factory pattern. It’s no
AbstractFactoryBean<T>
, though.
Endnotes
-
Since this example is so trivial, there are other ways to go about testing it. But I want to focus on the bare essentials of the problem and I ask you to bear with me, dear reader. ↩
-
Say what you will about SOLID, but dependency inversion is the hill I am willing to die on. ↩
-
This can happen if our type contains a raw pointer field. ↩
-
Matters would be different if we were interested in implementing
Send
andSync
on a typeT
that only implementsSend
. In this case, reachig forMutex<T>
is a solution. ↩ -
Yes, yes… those pesky other developers. Of course we would never introduce a bug ourselves, right? ;) ↩
-
Don’t get me wrong: you shouldn personally would only test the highest, most integrated, levels of our code. It’s good to test
execute_thread
in our example. But, to my mind, that should not absolve us from having to testspawn_thread
as well. ↩ -
We could also extend the
Audio
trait to provide a constructor. I don’t like this quite as much because different implementors of theAudio
trait might need different parameters for callableF
. ↩
Comments
You can comment on this post using your GitHub account.
Join the discussion for this article on this ticket. Comments appear on this page instantly.