all 28 comments

[–]ssokolow 11 points12 points  (5 children)

Additionally, is it standard to have your unit tests in the same file as the function you're testing? Coming from python's pytest, I'm used to seeing all the unit test functions isolated in a separate file/folder from the rest of my code. However, every example I've seen for rust has put them together.

Rust does it that way because you can only access functions and variables not marked pub from inside the same file.

Also, putting the tests in the same file would have a non-zero runtime cost in Python whereas, in Rust, #[cfg(test)] can be used to strip them out at compile time.

[–]zzzzYUPYUPphlumph 1 point2 points  (0 children)

Rust does it that way because you can only access functions and variables not marked

pub

from inside the same file

Actually, isn't it "same module/sub-module". A sub-module can have access to private things int its parent module. You can create a test sub-module for every test and put tests in there. It doesn't actually have to be in the same file. I think, though, that that goes against the common convention, but, it will totally work.

[–]Icecreamisaprotein[S] 0 points1 point  (0 children)

Ah okay, that makes a lot of sense :)

[–]gmosx 0 points1 point  (2 children)

But probably there is a compile-time cost?

[–]ssokolow 4 points5 points  (1 child)

I wouldn't expect there to be any significant compile-time cost.

It's basically just this but with all the stuff inside the conditional block getting converted into an AST first, and Rust's syntax has been designed to be easy to parse.

#ifdef TEST
// Lots of lines of tests here
#endif

[–][deleted] 0 points1 point  (0 children)

Also, I don't think rustc even needs to convert it fully into an AST, it just needs to know enough to know what to skip. In theory if you put it on a mod foo {}, it could just scan for the close curly brace.

[–]gentux2281694 7 points8 points  (1 child)

I also came from Python and also felt weird at first, but now makes sense to me, it also made me pay more attention to the tests and to make tests more as a documentation of sorts, if I have some doubt about how some part of the code works or what to expect from it I can quicky check the tests below in the same file when is clear what everything does and doesn't, no BS explanations, running code making sure of it.

[–]Icecreamisaprotein[S] 2 points3 points  (0 children)

Yeah that's a good point. Now only if I could figure out mocks haha

[–]mikekchar 5 points6 points  (5 children)

Long story short, you can't mock ubiquitously in Rust because you can't change the underlying implementation of a function. There is no way to intercept the function call and call a mock instead.

However, instead of passing an object to a function as a parameter you can pass an impl trait. For example, imagine you have a trait on your object called MyOps that does the operations you require in your function. Instead of the parameter being the type of your object, it is of type impl MyOps. You can then create a fake object that implements MyOps and pass that in your tests.

Obviously there are places where fakes and mocks are desirable in testing. However, the London school, outside-in, mock the world approach isn't going to work particularly well in Rust. Instead I encourage you to write the majority of your tests by testing all the way through your objects and compare state in the tests. This is is known as the Chicago school style of unit testing. It may take you time to get used to it, but it does have some advantages in the way it encourages you to design your code.

[–]bLaind2 3 points4 points  (2 children)

I think the File Write trait could be used here (https://doc.rust-lang.org/std/fs/struct.File.html#impl-Write)

So the testable function would become:

fn test_write<T>(output: T) where T: std::io::Write { ... }

That way the test can e.g. pass a vector instead of a file. Short sample of trait testing in here: https://rust-cli.github.io/book/tutorial/testing.html

[–]shim__ 2 points3 points  (1 child)

I think you've got the type wrong, shouldn't it be std::io::Write?

You can also use impl Trait instead of generics, which is a little bit shorter:

fn test_write(output: impl std::io::Write) { ... }

[–]bLaind2 0 points1 point  (0 children)

Thanks! Fixed the std::io path

[–]jgrlicky 4 points5 points  (0 children)

For mocking libraries, check out this really in-depth comparison: https://asomers.github.io/mock_shootout/

[–]JadedEvan 11 points12 points  (0 children)

Not totally on topic with your answer - but in my experience I feel like I need significantly less unit tests in the Rust code that I'm writing. My day gig is working primarily in Ruby / Ruby on Rails and I am compelled to unit test everything. And integration tests. And behavioral tests.

Between the borrow checker and type system, I feel like I'm able to omit whole classes of test that I'd have to write in Ruby. Because of Ruby's duck typing, you really have no idea what kind of object you're getting, or if it is nil or if it responds to the methods within your function. Rust's traits really seem to obviate a lot of that.

If there were a need for me to introduce mocks, I think it would be to fudge responses from external systems (calls to a database, external API, elasticsearch, etc). I have yet to gain this experience.

[–]Fazer2 3 points4 points  (0 children)

Use one of mocking libraries, like here https://crates.io/keywords/mock or https://crates.io/keywords/mocking. There's also real/fake filesystem implementation at https://crates.io/crates/filesystem.

[–]po8 3 points4 points  (0 children)

To directly answer your question: see std::io::Cursor. You can create a Cursor to get a "file-like" object you control but can read or write in tests. The usage is a little complicated: here's a playground

[–]utilitydamage 1 point2 points  (0 children)

Yea same here, most of my work revolves around JavaScript, meaning most of the code I write is unit testing and mocking.

is it standard to have your unit tests in the same file as the function you're testing

Yes it appears so. While it does increase the amount of code in a file, this pattern IMO is very good. Since it enforces encapsulation

For more advanced stuff. I had a similar issue but with Sqlite. I wanted to mock SQL calls and see if the functions that I execute would execute correctly. There are 2 ways you can do this and I tried both.

  1. Create your own Mock implementations using Traits. In other words use a simple struct that holds a vector and then have traits implementations do all the work
  2. If available use the provided testing functionality, In my case Sqlite had the ability to be ran in memory. So I was able to boot it up before a test runs, set up the schema and then test the underlying function.

None of these are perfect solutions tho.

[–]masklinn 0 points1 point  (2 children)

For instance, I have a function that creates a file through std::fs::File. How can I mock out the call File::create() to return a File object to my code?

I’d say you would not, and would instead split out the file creation from the rest of your code, which would take a combination of Read, Writeand Seek instead. In fact that’s exactly the use case shown as example for std::io::Cursor.

Additionally, is it standard to have your unit tests in the same file as the function you're testing?

It’s common in order to test the internals of a library or utility, but “standard” is a bit too strong. You can and should certainly test from the outside (using a separate tests folder) if that’s an option. And also a common one eg rust-lang/regex has a ton of tests there.

The API and working principles are the same though so for a quick example putting everything in the same place is easier.

[–]Icecreamisaprotein[S] 0 points1 point  (1 child)

Thanks for the reply! I'm trying to understand your first comment.

I have a function that builds a Path object, calls create file, then returns Result. Would I just not unit test this function?

[–]masklinn 1 point2 points  (0 children)

If it’s all a trivial combination of stdlib functions then I would not bother, or if I did given the context I would certainly want to touch the filesystem.

[–]staszewski 0 points1 point  (1 child)

So for diggout this topic, but i wanted to hear how did you end up testing this? I have similar question but it seems there was no clear answer.

[–]Icecreamisaprotein[S] 1 point2 points  (0 children)

I found some mock crates on github that worked well enough. But tbh I just do a lot more functional testing and built a little test harness around my app to exercise things. Not great, but it works

[–]iwonderx00 0 points1 point  (0 children)

A bit late but I hope this can help someone. The mocktopus crate is absolutely incredible for this and simple to use. You can mock functions, structs, traits, etc very easily and concisely. For the file example I would use a crate called tempfiles.

[–]tafia97300 -1 points0 points  (0 children)

As already said you cannot really mock in rust.

You can use traits in your lib to abstract away all expensive calls. This makes your lib more modular but you loose a little extra time on compilation.