In this blog post, we’ll explore strategies for organizing integration tests in Rust, addressing challenges like dead code warnings and maximizing modularity.
Integration Testing In Rust
Conventionally, integration test files are placed in tests
directory at the top level of a project.
Let’s create a project for illustration:
|
|
|
|
src/lib.rs
:
|
|
integration_tests.rs
:
|
|
cargo test
executes all tests in this project including ones in tests
directory.
How To Make A Utility File
As projects grow, the need to organize code into utility files becomes apparent. This section explores the creation of a utility file and how to prevent Cargo from treating it as an independent integration test.
Let’s say the project has grown big and you want to split code into multiple files and make util.rs
, extracting common functionalities. If you create tests/util.rs
and run cargo test
, the result will include the following section.
|
|
Cargo regards util.rs
as an integration test file. That’s because each file under tests
directory is compiled into an individual executable. The following command exemplifies it:
|
|
To avoid having unnecessary output, we can use mod.rs
to tell Cargo that it’s not an integration test. The folder structure will be like:
|
|
mod.rs
:
|
|
integration_tests.rs
uses setup_test()
with mod util;
:
|
|
This method is discussed in The Book’s Submodules in Integration Tests section.
Let’s see what happens when adding another utility function to mod.rs
:
|
|
Then, cargo test
warns that init_db()
is not used:
|
|
The warning remains even if there is a new file using both setup_test()
and init_db()
. The reason is that integration_test.rs
and mod.rs
are compiled as an independent crate, where init_db()
is not referred to.
To remove this warning, every test file must use every function in mod.rs,
which is hard to justify. Adding #[allow(dead_code)]
to all functions in mod.rs
is also not optimal. Some people complain about this cargo’s behavior (cargo test incorrectly warns for dead code), but resolving it seems not to be straightforward (as it comes from natural behavior when treating each file under tests
as a single crate).
However, there is a good way to address this issue.
One Integration Test Crate With Modules
Managing multiple crates in the tests
directory can lead to issues. Learning how to put tests into one crate with modules enhances organization and eliminates dead code warnings.
We can make a crate with submodules like:
|
|
Now there is only one crate named integration_tests
, whose source file is main.rs
.
main.rs
only declare submodules:
|
|
test_a.rs
is equivalent to integration_tests.rs
:
|
|
test_b.rs
is nothing special, but it uses init_db()
instead of setup_test()
:
|
|
cargo test
no longer warns dead code.
By following this structure, files can easily organized using the ordinal module system. Suppose that test_a.rs
has become bigger and should be divided. test_a
folder now has helper.rs
and submod.rs
:
|
|
test_a.rs
:
|
|
helper.rs
:
|
|
submod.rs
:
|
|
This idea comes from Zero To Production In Rust and Delete Cargo Integration Tests.
But why does this work? The reason is implicit Cargo’s convention.
Cargo’s Convention
As Cargo follows convention-over-configuration rules, it specially treats some files or folders by default, such as src/main.rs
or tests
. tests/<subdirectory>/main.rs
is a special file as well, and it enables integration tests to have multiple source files. We can confirm that <subdirectory>
is used as the name of the executable:
|
|
This convention is described in The Cargo Book.
Explicitly Specify The Source File In Cargo.toml
You can control the name of the source file by specifying the file path in Cargo.toml
:
|
|
In this example, tests/tests.rs
plays the same role as that of tests/main.rs
, and the name of the output executable is integration
.
In the real world, ripgrep uses this method. Feel free to check its Cargo.toml
and tests
if you are interested.
Conclusion
In this guide, we’ve covered various techniques for organizing Rust integration tests, addressing common challenges, and leveraging Cargo’s conventions. By adopting these strategies, you can maintain a clean, modular, and warning-free codebase for your Rust projects.
tests/util.rs
is considered an independent integration testtests/util/mod.rs
is not an integration test, but might causedead code
warningstests/integration_tests/main.rs
removes the warning and can leverage the module system- The naming rule can be altered using the
[[test]]
section inCargo.toml