Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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

  1. 1

    Cargo finds it by its name; the package now has a library crate named after the package (with - turned into _).

    Example

    The rainfall package gets an empty src/lib.rs. Its library crate is named rainfall, so main.rs can later write use rainfall::Config;. A package named rain-gauge would give a library named rain_gauge.

    Why: a library is the only kind of crate that tests in tests/, and other programs, can depend on.

  2. 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 main looped over the file's lines, added them up, printed the total, and called process::exit(1) on a bad line. That loop becomes pub fn summarize(text: &str) -> Result<Summary, RainError>. A bad line is now the value Err(RainError::BadLine(2, ...)). So a test can check that summarize("2.5\n0\n11.0\n") gives days == 3 and total_mm == 13.5.

    Why: a function that prints or exits can't be checked by a test; one that returns can.

  3. 3

    Gather the settings into a pub struct Config, built by Config::from_args(&[String]) -> Result<Config, _>.

    Example

    Config { path, inches } is built by Config::from_args(&args). A test passes &[String::from("--inches")], which has no file name, and checks that the answer is an Err. No terminal is needed.

    Why: taking a slice instead of calling env::args() lets a test pass in any arguments.

  4. 4

    It reads input, calls the work, prints the output, and returns the first error.

    Example

    run(&config) reads the file, calls summarize, and prints. For week.txt it prints 7 days, 24.3 mm and wettest: day 3. For a missing june.txt it prints nothing and returns Err(RainError::Read(...)).

    Why: it gives main exactly one thing to call and one error to report.

  5. 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's main is now about fifteen lines. With no arguments it prints the usage message and returns ExitCode::from(2). With june.txt it prints error: can't read june.txt: ... and returns ExitCode::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.