How would you implement an application for easy development and testing without adding much "dev" code into the application?

This question is aimed at finding out people’s approaches / recommendations for good practice, than a call for help.

I have a binary that loads configuration / assets, which should be next to the binary when installed. In development, the built binary lives in target/<profile>/<binary>[.exe].

Problem 1:

I want to be able to run cargo test [--release] to test different sets of configuration.

Approach I’ve taken:

  • In unit tests, I make an intermediate function which lets me pass in PathBuf so I can inject a path to a fake exe parent directory, to point it to a tempdir. The delta between the test code and the real code is “how to calculate the parent directory”.

Problem 2:

I want to run cargo run [--release] without additional “fluff”. Additional “fluff” being copying configuration to the target/<profile> directory. I don’t want to do that because:

  • It’s incovenient / easy to forget a step / have stale config.
  • It’s not isolated if I wanted to run things in parallel, e.g. have integration tests that execute cargo run.

Approach taken:

  • In #[cfg(test)] mode, or when using the debug profile, tell the application to also search option_env!("OUT_DIR") and option_env!("CARGO_MANIFEST_DIR") for default config.

  • When using the release profile, look up an APP_DIR environmental variable to substitute the application’s parent directory.

    The environmental variable allows the code to not contain information about the build environment, as well as allows tests in the tests/ directory to do something like:

    1. Set APP_DIR=path/to/some/directory.
    2. Run cargo run [--release].
    3. Do assertions on the actual binary that will be released.

    Humans still have to go export APP_DIR=`pwd`; cargo run --release to use the crate dev config.

This does introduce an additional APP_DIR input to the application. I guess git does allow you to do something similar through its --git-dir parameter.

Problem 3:

When running unit tests in the release profile, library dependencies are not cfg(test), even if they are in the same crate workspace, so they don’t pass the cfg!(test) check to use dev config.

That’s hard to understand in words, so here is a diagram:

// A -> B means A depends on B
lib_a -> lib_b -> lib_macro
  • lib_macro provides a macro to load configuration from the current crate directory if it’s in test mode or debug mode.
  • lib_b loads configuration type B
  • lib_a uses lib_b

I want lib_a to store configuration for lib_b for tests. In cargo test, it loads fine from lib_a's crate directory because lib_b is compiled using the debug profile. However when using cargo test --release, it doesn’t load fine, because in release mode, lib_macro doesn’t insert the CARGO_MANIFEST_DIR value as a base directory (to prevent leaking development values).

So, the approach I took was:

  • Make lib_a set the APP_DIR environmental variable during tests.

This didn’t feel clean, I don’t know why.

If I understand correctly, your tests run an actual executable built by Cargo.

My approach for that is not to run any executables. Instead, make it a Rust library and move 99% of code there, and make the bin target just load your library in main() and run it with arguments. This way all your tests can run your app’s code directly and override all inputs.