9

As suggested by The Book, I have moved the integration tests in my crate to a tests directory. Some of those tests use functions that I don't want to export outside of the crate, though, and I am no longer able to use them in the integration test folder. I use them for non-test purposes too, so they need to compile outside of tests too. I tried using variants of pub(restricted), but I wasn't able to make it work. Ideally I'd like to have something like pub(tests).

directory tree (the relevant bits):

my_crate
|- src
   |- parser.rs
|- tests
   |- parsing.rs
|- benches
   |- parsing.rs

tests/parsing.rs:

extern crate my_crate;

use my_crate::parser::foo;

#[test]
fn temp() {
    foo();
}

benches/parsing.rs:

#![feature(test)]
extern crate test;
extern crate my_crate;

use test::Bencher;
use my_crate::parser::foo;

#[bench]
fn temp(b: &mut Bencher) {
    b.iter(|| { foo(); });
}

My current workaround is to make the relevant objects public and invisible in the docs (#[doc(hidden)]), but it doesn't convey the proper intention. Can I make an object public only for integration test / benchmarking purposes?

ljedrz
  • 20,316
  • 4
  • 69
  • 97
  • Have you tried `#[cfg(test)]` on them? – Matthieu M. Dec 07 '17 at 15:36
  • @MatthieuM. I also need them for non-test purposes. – ljedrz Dec 07 '17 at 15:37
  • Hum... not sure if it's possible to only put the condition on the `pub` and I guess duplicating the definition is not an option :x – Matthieu M. Dec 07 '17 at 15:55
  • 1
    *use functions that I don't want to export outside of the crate* — then it's **not** an integration test. The purpose of an integration test is to exercise the code *as a user would*. – Shepmaster Dec 07 '17 at 15:58
  • @Shepmaster tbh this popped up in my benchmark tests and I thought it could have been `Bencher`-specific, but I checked the same for `tests` and the result was identical. Since the latter is easier to analyze I picked it as the main theme. – ljedrz Dec 07 '17 at 16:10

2 Answers2

8

One difference between integration tests and unit tests is that integration tests is supposed to test the "public API" of your crate only. Tests for internal functions is fine to keep together with the functions themselves in the src tree.

If you want to keep them a little separate, you can use a test submodule to the module containing the functions to test, as private parts are available to submodules.

If you still really want to have internal / unit tests in the tests in the tests directory, you can use a feature flag to enable public wrappers of internal functions to test (and mark the tests with the same feature flag). Something like this in the code:

#[cfg(feature = "testable_privates")]
pub fn exposed_something(args) {
    something_private(args)
}

Then in your test methods, you can import and call exposed_something. If the feature testable_privates is not defined, your tests will fail to compile. To solve that, use the feature flag to make the tests conditional as well;

#[cfg(feature = "testable_privates")]
#[test]
fn test_something() {
    assert_eq!(exposed_something(my_args), expected_result)
}

Also, before doing that you need to define the feature in your Cargo.toml, like so:

[features]
testable_privates = []

(The empty array is to signify that the feature does not require any otherwise optional dependencies).

Now, if you just run cargo test, both exposed_something and test_something will just be silently ignored, but if you run cargo test --features testable_privates, they will be compiled and tested.

As you see, this gets rather complicated, so I really think it is a better idea to just test public aspects of your crates from tests and keep tests of private methods close to those methods themselves in src.

JonathanDavidArndt
  • 2,518
  • 13
  • 37
  • 49
Rasmus Kaj
  • 4,224
  • 1
  • 20
  • 23
  • Wrapper functions (even with just `#[cfg(test)]`) are a valid solution for functions, but not so much for `enum`s or `struct`s. Until something like `pub(specific_external_crate)` becomes available I guess that I'll just need to rearrange my tests and benchmarks a bit :). – ljedrz Dec 07 '17 at 17:27
  • This is a bit unfortunate. There's no reason integration tests shouldn't have access to internal functions to verify that the test succeeded. It can make them a lot faster and easier to write. Ignoring `#[cfg(test)]` just makes that difficult and I can't see any benefit. :-/ – Timmmm Apr 27 '21 at 20:27
  • I think it makes complete sense, if we let go of the unit/integation terminology and look at the tests at what they really are: _Internal_ tests in `/src` and _external_ tests in `/tests`. The fact that external tests only access your public api is the whole difference. – Rasmus Kaj May 24 '21 at 22:16
-1

You can probably do it by adding a public module that only exists when testing and that re-exports the required symbols. Something like:

#[cfg(test)]
pub mod testing_parser {
    pub use parser::foo;
}

Then use my_crate::testing_parser::foo in your tests.

Jmb
  • 18,893
  • 2
  • 28
  • 55
  • Unfortunately I'm getting `error[E0364]: parse is private, and cannot be reexported` and `note: consider marking parse as pub in the imported module`. – ljedrz Dec 07 '17 at 16:53
  • 2
    Well, you could mark `parse` as `pub` and so long as you put it in a private module, it won't be accessible directly from outside your crate, but it can be re-exported. Unfortunately, I just checked and `#[cfg(test)]` items are disabled for integration tests (i.e. for tests that live in the `tests` folder), so you will probably need to use a `feature` to enable the re-export module as suggested by Rasmus Kaj. – Jmb Dec 08 '17 at 14:03