53

I created a new binary using Cargo:

cargo new my_binary --bin

A function in my_binary/src/main.rs can be used for a test:

fn function_from_main() {
    println!("Test OK");
}

#[test]
fn my_test() {
    function_from_main();
}

And cargo test -- --nocapture runs the test as expected.

What's the most straightforward way to move this test into a separate file, (keeping function_from_main in my_binary/src/main.rs)?

I tried to do this but am not sure how to make my_test call function_from_main from a separate file.

ideasman42
  • 42,413
  • 44
  • 197
  • 320

5 Answers5

78

The Rust Programming Language has a chapter dedicated to testing which you should read to gain a baseline understanding.


It's common to put unit tests (tests that are more allowed to access internals of your code) into a test module in each specific file:

fn function_from_main() {
    println!("Test OK");
}

#[cfg(test)]
mod test {
    use super::*;
    
    #[test]
    fn my_test() {
        function_from_main();
    }
}

Modules can be moved to new files, although this is uncommon for the unit test module:

main.rs

fn function_from_main() {
    println!("Test OK");
}

#[cfg(test)]
mod test;

test.rs

use super::*;

#[test]
fn my_test() {
    function_from_main();
}

See Separating Modules into Different Files for detailed information on how files and modules map to each other.


The more common case for tests in a separate file are integration tests. These are also covered in the book by a section devoted to tests outside of the crate. These types of tests are well-suited for exercising the code as a consumer of your code would.

That section of the documentation includes an introductory example and descriptive text:

We create a tests directory at the top level of our project directory, next to src. Cargo knows to look for integration test files in this directory. We can then make as many test files as we want to in this directory, and Cargo will compile each of the files as an individual crate.

Let’s create an integration test. With the code in Listing 11-12 still in the src/lib.rs file, make a tests directory, create a new file named tests/integration_test.rs, and enter the code in Listing 11-13:

Filename: tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

Listing 11-13: An integration test of a function in the adder crate

We’ve added use adder at the top of the code, which we didn’t need in the unit tests. The reason is that each test in the tests directory is a separate crate, so we need to bring our library into each test crate’s scope.

Note that the function is called as adder::add_two. Further details about Rust's module system can be found in the Packages, Crates, and Modules chapter.

Since these tests exercise your crate as a user would, if you want to test a binary, you should be executing the binary. Crates like assert_cmd can help reduce the pain of this type of test.

In other cases, you should break your large binary into a large library and a small binary. You can then write integration tests for the public API of your library.

See also:

Timmmm
  • 88,195
  • 71
  • 364
  • 509
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 5
    How to write the integration tests (the tests inside to `tests` directory) for an *executable* (not *lib*)? `extern crate ` does not work then. – Nawaz Sep 30 '18 at 16:59
  • 4
    @Nawaz if you are performing *integration* tests of an executable, you drive it as the user would: by running the built command via something like `std::process::Command`. Otherwise you are talking about unit tests. – Shepmaster Oct 07 '18 at 15:56
  • Using a debugger with an integration test testing a bin by spawning a separate process is a suboptimal developer experience. :-( – Squirrel May 20 '20 at 07:26
  • @Shepmaster, I am not able to understand the difference between your answer and @mmai answer. Could you explain why naming the tests file `test.rs` works and naming it `foo_test.rs` does not work without path? – Tamil Vendhan Kanagarasu Jul 01 '22 at 15:26
  • @TamilVendhanKanagarasu because the language expects module *filenames* to match the module *names*. When you go outside the convention, how would the compiler know which file to look in? I would generally discourage people from using the `#[path]` attribute except in *very specific* cases (and this is not one of them). – Shepmaster Jul 01 '22 at 17:37
24

If you have a module foo.rs and want to place your unit tests next to it in a file called foo_test.rs, you'll find that this isn't always the place that Rust will look for a child module.

You can use the #[path] attribute to specify the location of the file corresponding to the module:

#[cfg(test)]
#[path = "./foo_test.rs"]
mod foo_test;

This is explained in the blog post Better location for unit tests in Rust.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
mmai
  • 715
  • 1
  • 7
  • 15
  • 10
    Why do you want to go out of your way to put files in non-idiomatic locations? Place it as `foo.rs` and `foo/tests.rs`, just like every other module. – Shepmaster Jun 25 '19 at 15:13
  • 3
    No irritation here as I don't have to work on your code! I just find the going to be much smoother if I follow the idioms of the language, not trying to carry along all of the baggage from each previous language. – Shepmaster Jun 25 '19 at 16:05
  • Why is `#[path]` needed? Most people would reach for `mod foo_test` because it looks the same as what was in `lib.rs`, but they get a `file not found error`. This happens because when you use `mod foo`, all code in `foo.rs` is now in the scope of the module `foo` - which effects where `mod` searches in. So while in the scope of module `foo`, `mod foo_test` looks for `src/foo/mod_test.rs`, and `src/foo/mod_test/mod.rs`. A little hard to wrap the head around. Also, in `foo_test.rs` you then write `use super::my_func` to access members of `foo.rs` (the `foo` module). – vaughan Sep 08 '21 at 13:54
  • @Shepmaster i think this is a pretty cool possibility to allow ppl to keep the unit tests separately from its source, to prevent polluting the source. Actually this is the first a few questions i searched after i reading the rust book, i could seriously black rust without this possibility, hardly believe messing the unit tests and source together is a more maintainable way – lnshi Oct 19 '21 at 03:28
  • From the rust book: "The convention is to create a module named tests in each file to contain the test functions and to annotate the module with cfg(test)." I would simply follow the "convention." Non-idiomatic practices make it harder for someone else who gets into your code. – Aaron May 24 '22 at 06:07
  • 2
    It's nice that Rust is batteries-included for tests, but it doesn't but follows conventions of all the other test frameworks. It's good practice to separate code into different files to aid in organization; this holds for tests as well. Personally I want to be able to page up/page down through the file and see key points, I don't want to have to read lines which are not relevant to helping me understand functioning of the module - especially when my tests make up more than half of the lines. To me this is worth breaking from idiomatic Rust use; ymmv. – Codebling Jan 09 '23 at 19:43
3

You're right; function_from_main is inaccessible outside of main.rs.

You need to create an src/lib.rs and move the functions you want to test piecemeal. Then you'll be able to use extern crate my_binary; from your test module, and have your functions appear under the my_binary namespace.

Tobu
  • 24,771
  • 4
  • 91
  • 98
2

I do believe you should follow the advice of reading the Rust book chapter on testing, however, that still won't quite answer your question, how to separate test and source files.

So say you have a lib.rs source file and wanted a test_lib.rs file. To do this all you need is:

In your lib.rs:

mod test_lib;

// rest of source

Then in your test_lib.rs:

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

#[test]
fn test1() {
    // test logic 
}

// more tests

Then, both you and cargo test should be happy.

Rock Boynton
  • 51
  • 1
  • 5
0

To embellish* @mmai's answer:

#[rustfmt::skip]
#[cfg(test)]
#[path = "./foo_test.rs"]
mod foo_test;

cargo fmt seems to be unhappy following the path to the new test file. If anyone has a better solution I'd be happy to hear it, because I wish cargo fmt would format my test files too!

*Sorry: I'd add this just as a comment on the existing answer but I don't have enough reputation.

  • I'm not sure what this answer adds to mmai's answer. Also, please don't ask follow-up questions in answers. That's a discussion forum style, which is not appropriate for the Q&A format on this site. – cigien Jan 06 '22 at 12:38
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 06 '22 at 13:10
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/30751950) – Akida Jan 07 '22 at 08:32