Recipe
Split a program into a library and a thin main
Use when: a program's logic lives in main.rs and you want to test it, or reuse it in a second program
Goal: lib.rs holding Config, the work and run, and main.rs holding only the arguments, the call and the error reporting
Taught in Splitting a binary for testability
Quick recipe
-
1
Cargo finds it by its name; the package now has a library crate named after the package (with - turned into _).
Example
The
rainfallpackage gets an emptysrc/lib.rs. Its library crate is namedrainfall, somain.rscan later writeuse rainfall::Config;. A package namedrain-gaugewould give a library namedrain_gauge.Why: a library is the only kind of crate that tests in tests/, and other programs, can depend on.
-
2
Move every function that computes something into the library, marked pub. Replace printing with a returned value, and process::exit with an Err.
Example
The old
mainlooped over the file's lines, added them up, printed the total, and calledprocess::exit(1)on a bad line. That loop becomespub fn summarize(text: &str) -> Result<Summary, RainError>. A bad line is now the valueErr(RainError::BadLine(2, ...)). So a test can check thatsummarize("2.5\n0\n11.0\n")givesdays == 3andtotal_mm == 13.5.Why: a function that prints or exits can't be checked by a test; one that returns can.
-
3
Gather the settings into a pub struct Config, built by Config::from_args(&[String]) -> Result<Config, _>.
Example
Config { path, inches }is built byConfig::from_args(&args). A test passes&[String::from("--inches")], which has no file name, and checks that the answer is anErr. No terminal is needed.Why: taking a slice instead of calling env::args() lets a test pass in any arguments.
-
4
It reads input, calls the work, prints the output, and returns the first error.
Example
run(&config)reads the file, callssummarize, and prints. Forweek.txtit prints7 days, 24.3 mmandwettest: day 3. For a missingjune.txtit prints nothing and returnsErr(RainError::Read(...)).Why: it gives main exactly one thing to call and one error to report.
-
5
main collects env::args(), calls Config::from_args and run, and on failure prints the message and returns an ExitCode (2 for usage, 1 for other failures).
Example
rainfall'smainis now about fifteen lines. With no arguments it prints the usage message and returnsExitCode::from(2). Withjune.txtit printserror: can't read june.txt: ...and returnsExitCode::FAILURE, which is status 1.Why: what's left is the part only a running program can do, and it's short enough to check by reading.
Common mistakes
- Calling process::exit or panicking inside the library for an ordinary failure.
- Leaving printing inside the calculation, so a test has nothing to compare.
- Writing crate::Config in main.rs, where crate means the binary; the library is named by the package's name.