Mocking in Rust & thread-local globals


#1

G’day all,

I know there’s been an almost annual discussion of mocking in Rust on Reddit, though I have a specific question regarding implementation.

I have a really basic framework to help me mock parts of std::fs for unit tests. However I need some kind of mutable global that I can use to setup and monitor the state of the mocked fs module. I could use something like lazy-static, though as unit tests must be run in isolation, the global has to actually be ‘local’ to the thread running the test.

For example, if I mock out the function “fs::metadata”, I need to be able to tell it what path to expect, and check whether it has been called for a specific test, i.e. a specific thread.

I could solve this by doing away with the idea of global state and passing references around, though then I would have to create two function calls every time I interfaced with any module I wanted to mock - 1 for production and 1 for tests. That sucks!

Does anyone have any ideas, or is this a bad approach?

Cheers,

Pete.


#2

I had a similar problem when working on a constant evaluator for clippy. However, after some mucking around (including using undefined behavior!), we just used an Option<&'t Context<..>>, which made the code simple and extensible. However, you may have different requirements.

If you want to mock fs::metadata, why not use a trait that you implement for both fs::metadata and your own mock::metadata?


#3

I thought about using traits, though I can’t implement them on functions can I?

When you say you used an Option, I’m envisioning something like this:

fn foo(mock_data: Option<...>) {
    if mock_data.is_some() {
        metadata("/path", mock_data);
    } else {
        metadata("/path");
    }
}

Is that what you meant?

You’ll have to forgive me for the dumb questions. I am new to Rust and of diminished IQ! :smile:


#4

No, there are only the Fn* traits available for functions. However, you can extract the file-to-metadata operation from your function (because you trust it to work, don’t you?) and test the rest. E.g.

trait MetaLen {
    fn len(&self) -> usize;
}

impl MetaLen for fs::Metadata {
    fn len(&self) -> usize { self.len() }
}

struct Mockmetadata { ... }

impl MetaLen for MockMetadata {
    fn len(&self) -> usize { .. }
}

fn<T: MetaLen> my_length_consuming_function(x: &t) { ... }

pub fn my_public_function(x: &str) {
    fs::metadata(x).map(my_length_consuming_function); // easiest case
}

#5

Building on that:

trait MetadataProvider {
    fn metadata(x: &str) -> Option<Self>;
}

struct IOMetadata {
    metadata: fs::Metadata,
}
impl MetadataProvider for IOMetadata {
    fn metadata(x: &str) -> Option<IOMetadata> {
        fs::metadata(x).map(|m| IOMetadata { metadata: m })
    }
}

struct MockMetadata;
impl MetadataProvider  for MockMetadata {
    fn metadata(x: &str) -> Option<MockMetadata> {
        Some(MockMetadata)
    }
}

pub fn my_public_function<T: MetadataProvide + MetaLen>(x: &str) {
    T::metadata(x).as_ref().map(my_length_consuming_function);
}

my_public_function::<MockMetadata>("foo.ext");
my_public_function::<IOMetadata>("foo.ext");

#6

Thanks for both of your feedback, though all this manoeuvring around types is hurting my head.

jdm, I don’t understand why your IOMetadata struct has a seemingly unused “metadata” attribute, or why the MockMetadata struct doesn’t. Also your implementation of MetadataProvider for IOMetadata returns the result of fs::metadata(), which is an io::Result, not an Option. How does this work?


#7

Sorry, I was writing code off the cuff without being aware of the types involved. The continuation to show how it connects to @llogiq’s example looks like this:

impl MetaLen for IOMetadata {
    fn len(&self) -> usize { self.metadata.len() }
}

impl MetaLen for MockMetadata {
    fn len(&self) -> usize { ... }
}

Edit: here is a playground link that demonstrates it compiling.


#8

Cheers mate! I really appreciate it.


#9

Wait, do you just need thread-local variables? Because you can have them with the `thread_local!`` macro (example), btw it’s stable.