this post was submitted on 04 Aug 2023
2 points (100.0% liked)

Rust

6137 readers
13 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

[email protected]

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 2 years ago
MODERATORS
 

I know there's mockall which seems to be geared towards implementation methods and traits, but I'm wondering about just structs with non-function properties.

In my tests, I want to define initialized structs with values. It works fine to just do it like I normally would in code, but I'm wondering if there's more to it than that. Like if I have a cat struct:

struct Cat { name : String } `

#[cfg(test)] pub mod test { use super::Cat; fn test_create_cat() -> Cat { Cat { name. : String::from("Fred") }; }

That's fine, but should I be doing it differently? What about mockall, is it not meant for structs with properties?

top 9 comments
sorted by: hot top controversial new old
[–] [email protected] 4 points 1 year ago (1 children)

There's no special magical way to mock things in Rust. What you're doing makes sense. Rust's (built-in) unit testing story is not very complex, you have 3 assertion macros and the #[test] attribute basically.

What additional functionality are you hoping to get through an alternative means of initializing the cat?

[–] [email protected] 1 points 1 year ago

Nothing I guess. I just was thinking there would be more to it than that.

[–] [email protected] 3 points 1 year ago (2 children)

I'm not quite sure I fully understand the question, but would implementing the Default trait work?

Otherwise, I've used Arbitrary in tests before, maybe that's what you're looking for?

[–] [email protected] 1 points 1 year ago

Default could be useful here, thanks!

[–] [email protected] 1 points 1 year ago

Oh wow, default is so nice. I wasn't exactly looking for this when I asked the question, but I'm glad you tipped me off to it.

[–] [email protected] 2 points 1 year ago

Just FYI -- your test isn't going to run, you need to mark it with #[test].

So if you're used to a language like JS or python, or even Java, you're going to be a bit frustrated at how to mock things in rust. In those languages everything is boxed. In JS or python, because they're dynamically typed, you don't have to do anything special to mock, and in Java you can either play nice and use interfacees everywhere, or else you can do some runtime magic to mock an object of a regular class.

You can do something similar in rust -- e.g. you can have a trait Cat and a struct RealCat and a (or possibly many) struct FakeCat. (There are crates that will help you with this). Then you need to either accept a Box or a &dyn Cat, or make your code under test generic (which can infect the rest of your code if you aren't careful), something like fn uses_a_cat(cat: C) {}

So there's not quite as easy of an answer. You also have several more options, for example you can

pub struct FakeCat;

pub struct RealCat;

#[cfg(test)]
pub type Cat = FakeCat;

#[cfg(not(test))]
pub type Cat = RealCat;

and get a fake (or mock, or spy, whatever test double you'd like) in all test code in your same crate. This doesn't work well across crate boundaries though, and it only lets you provide one double, so it makes sense for that double to be very generic (there are crates to do this for you as well).

So there's not really a one-size-fits-all approach. You have to think about the tradeoffs.

However I think the best overall test strategy (and it doesn't always apply, but it should be preferred when it does), is the same one used for functional programming: just accept and return values. Pure functions don't need mocks, and even impure functions can easily be tested if they don't have other side effects that you need to prevent during tests. Obviously you still need to deal with side effects if your program is going to work, but if you have lots of pure unit tests that don't need any fancy test doubles you can do end-to-end testing for all of your I/O and other messy side effects. Which as I said, doesn't always apply (sometimes you really need test doubles), but it's good to use whenever possible.

[–] [email protected] 2 points 1 year ago (1 children)

I don't think you can mock a struct. I'm guessing a mock would be a different struct type with the same fields, or a subset of fields. But since Rust types are nominative, not structural, you can't substitute one struct type with another even if the data is identical.

When you need a struct for a test you create a value of the original type. Some problems that might come up are,

  • you need to call a constructor to create a struct, and it has side-effects that you don't want to run in the test
  • you need to call a constructor that is not visible to the test
  • the struct requires data that is difficult or cumbersome to produce in the test

For the first two problems one option is to use conditional compilation to create test-only constructors that bypass code you don't want to run, or that have broader visibility.

For the last problem you might want to refactor the code to use traits which you can create mock/test implementations for. Or you might change the struct to make the problematic data optional, or lazily initialized.

[–] [email protected] 2 points 1 year ago (1 children)

This is a great answer, thanks. I'll have to look more into conditional compilation. That's new to me.

[–] [email protected] 1 points 1 year ago

A few days later, but keep in mind that if you write your tests in the module you declare your structs, you'll have access to its "private" (non-pub) members since those are technically module scoped (default scope is pub(self)).

pub struct Cat {
    name: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn create_cat() {
        let cat = Cat {
            name: "fluffy".into(),
        };
    }
}

Playground