It seems ok for trivial/toy code. However, for real life code (with large files and complex folder structures), it makes source files quite large doubling or tripling the lines of code and makes it hard to navigate and look at test and code side by side (at least for me). Also, it makes it difficult to understand production and test code's dependencies on use of crates.
In other languages (from my experience esp. C++ and Python) the usual convention is to separate production and test code in separate files (and a few conventions around the folder structure, typically some flavour of these 2 structures:
I'd love to learn what conventions do people follow in real-world large-scale Rust projects that have large code base, with long source files, and a complex package/workspace structure. thanks
I would say do as makes sense for your project. I've seen bigger code bases like tokio, which don't only put unit test modules into their own files, but even modularise the unit test module into sensible sized submodules, when the unit test suite becomes too big:
I find that by the time the mod tests is big enough that it shouldn't be in the same file, it's often time to split the module entirely into more module files.
src/
something/
mod.rs
part1.rs
part2.rs
tests.rs
Continue to use mod tests, but let it be mod tests; instead of mod tests {}.
Doesn't that mean tests will no longer have access to private items in part1 and part2? Or would you put those tests that need access to private items in a mod tests {} inside part1 and part2?
No, the module you specify in pub(in) has to be a common ancestor of the module where the item is defined and the module where will be used. When I said pub(in crate::something) I was literally referring to the mod something of my previous example — sorry if that was confusing!
Ah right, because contained modules already have private access! I always forget that. I'd expect super::tests should work for the common tests.ts example, through?
I rarely put them in the same file, except for small units like helper functions or tools. I have several reasons for that:
It's much easier to jump between two files when I write the tests and the code to make them pass. In comparison, jumping between two moving places in the same file is cumbersome.
It's also easier to search and read the unit tests and the source code side-by-side if you want to know what the code is supposed to do.
The tests are often longer than the code, so it tends to drown the source in the amount of tests when they're both in the same file. Even without that, files would become very long more quickly, which prompts to split them for the wrong reason.
It's easier to ensure that everything is test-related in a file with a single #![cfg(test)] at the top, even test helper functions that I share between modules.
It's easier to follow the history of a source file's modifications without any interference from test modifications.
I just find that file organization cleaner than mixing two entirely different things. That must come from a long habit of separating test files in various disciplines, like you (including projects where the one writing the code and the one writing the tests were not the same person).
For the structure, it depends a lot on the project, but unit tests go in tests.rs files, semantically the same as a module in the source file. Also, in Rust you can conveniently isolate the integration tests under the tests root directory (vs your two models above in other languages), where they get the same visibility as the lib user, so that's a good way to check that everything is accessible—or not—as planned. That's also where I put the tests that fail to compile when I'm using the trybuild crate.
Oh, I see where I was confused. Not starting from a common ancestor, resolving to one. So pub(in super) = pub(super), or pub(in super::super) if you're nasty. Seems very and strangely limited - not much reason to use it over the shorthands.
I guess just having a mod test; for each module is the least weird.
I tried Option 2 where production code are unit tests are in different packages/crates is also do-able but didn't seem natural or ergonomic.
@jumpnbrownweasel: I generally prefer not to test private/internal code, only the 'publicly' observable behaviour. And so by design where possible I try to make such internal dependencies injectable by parametrizing them(usually via templates/generics) and then define usable/intended type as pub. e.g.:
pub struct MyGenericFeature<T1, Algo1, Algo2>
where ...
{...}
pub type MyConcreteFeature = MyGenericFeature<ConcreteType, Closure1, Closure2>;
mod tests{
type MyFeatureTestedWithMockAlgo1 = MyGenericFeature<ConcreteType, MockClosure1, Closure2>;
...
}
I guess the downside of this approach is that the template parameters (E.g. ConcreteType, Closure1, Closure2) need to be pub (that said, it's internals don't).
It depends. When a project grows, it's not practical to test everything from the visible API any more because you'd need to produce an awful lot of stimuli to get a good coverage, which is inefficient and unsafe. That's why you divide and conquer with unit tests (or, if you work in a team, it's just the only option).
Unless you're making all your code public, but that's another problem because then any change is considered an API change, which is very annoying for all the downstream users. One rule of development is "expose as little as strictly necessary".
In a simplified TDD methodology, integration tests are mostly acceptance tests (though they can be more than that), so it's more of a contractual role, while unit tests are what makes you progress during your development until you can fulfil all the integration tests.
I think it's a sane and safe practice to have good unit tests for private code, except for simple cases.
My previous wording was not so clear. I meant to say, I try not to unit-test internal implementation detail of a struct/function (units), only it's external interface. I didn't meant that that I only test public interface of the of the library, I meant I test each unit only via its "public" interface (whether public exposed or not as part of library) and use 'dependency injection' via templates where alternative stimuli is needed. Similar to what @Redglyph said, if internal implementation details need to be tested, probably the unit in question probably needs to be broken down into smaller units and tested separately. So yes, in order to achieve this, I still need pub(crate) or
E.g. If I was implementing a custom a HashGraph data structure, I try to templatise the HashGraph<HashAlgorithm> and ensure separation of concerns. This way:
I can test hash algorithm separately if needed (in my case though I'm using DefaultHasher from std so I dont need to test it)
I inject a HashGraph<DummyHighCollisionHashAlgorithm> to test the collision handling connection/disconnection, traversal, errors and exceptions, all of it using the public interface of HashGraph
Then I write benchmark and sanity tests with the HashGraph<RealHashAlgorithm>
I would not necessarily test how HashGraph it implements data storage (e.g. internally could use Vec of raw pointers or indexes or keys to store the connections and it could all be an internal module, but in my specific case it is an 'unimportant' implementation detail).
I try to take this approach with all non-trivial components of my big library/app/system.
For my own projects, I put my tests in the same source file as the production code, and I modularize the tests into sensible sized units. Yes, the tests more than dwarfs the production code. That's expected and normal.
My editor of choice (both Vim and Emacs) allows me to open multiple views on the same source file, allowing me to easily view the production code and tests side by side. However, I rarely even do this. If I'm adding a feature or fixing a bug, the first thing I do exclusively is adjust the tests to reproduce the problem and thus fail the tests. This makes sure of several things: (1) I understand the problem, and (2) makes sure that the feature/bug-fix isn't already in the code. Then, and only then, do I start working on the production code to make the test fail. I almost never have an overt need to read the tests and production code side by side.
For those moments when I do need to collapse the size of the file, I make use of my editor's "code folding" functionality. This is another option, which allows you to collapse many lines of code as being irrelevant to your immediate interests, and thus hides them from view (you can always re-open the fold if required). Most IDEs support at least this, as well as Vim and Emacs of course.
Note that source-aware editors will recognize mod blocks and will let you fold entire modules as well.
For rapid top-to-bottom or vice-versa navigation, I almost always make use of ordinary text search. It's just so much faster. Or, I'll remember line numbers from error reports, again, allowing me to jump directly to a problematic line.
There are always exceptions to the rule; but these are my daily driving methods, and honestly, the size of a source file is very rarely a problem for me.