Level 3 · Power Tools

Cargo, Modules & Tests 🚢

You can write real Rust. The last skill is shipping it: organizing code that grows, proving it works, and standing on the shoulders of 300,000 crates.

Modules: rooms in the house 🏠

One giant main.rs stops scaling around a few hundred lines. Modules give your code rooms: related functions live together, and pub marks which doors are open to visitors:

mod kitchen {
    pub fn cook() -> String {          // pub = usable from outside
        format!("cooking with {}", secret_spice())
    }

    fn secret_spice() -> &'static str { // private: kitchen business only
        "old bay, obviously"
    }
}

fn main() {
    println!("{}", kitchen::cook());
    // kitchen::secret_spice();  // ❌ private: the compiler guards the door
}

When a room outgrows the house, move it to its own file: mod kitchen; in main.rs tells Cargo to look in src/kitchen.rs. Same code, tidier home. The use keyword makes paths shorter: use kitchen::cook; is exactly what use std::collections::HashMap; was doing all along.

Tests: proof, not hope 🧪

Testing is built into the language. No frameworks to install, no excuses:

fn tip(bill: f64, percent: f64) -> f64 {
    bill * percent / 100.0
}

#[cfg(test)]                    // only compiled when testing
mod tests {
    use super::*;               // bring in the code above

    #[test]
    fn normal_tip() {
        assert_eq!(tip(100.0, 20.0), 20.0);
    }

    #[test]
    fn zero_percent_is_zero() {
        assert_eq!(tip(100.0, 0.0), 0.0);
    }
}
$ cargo test
running 2 tests
test tests::normal_tip ... ok
test tests::zero_percent_is_zero ... ok

test result: ok. 2 passed; 0 failed

The toolbox: assert_eq!(a, b) (must be equal); assert!(cond) (must be true); #[should_panic] (this test expects a crash). Write a test the moment you write a function; future-you will refactor without fear, because the tests will catch any slip. (This very website's Rust server has tests: run cargo test in the project and watch them pass.)

Crates.io: the world's toolshed 📦

A crate is a Rust package, and crates.io hosts over 300,000 of them, free. Adding one is a single command; here's the beloved rand:

cargo add rand
use rand::Rng;   // trait import, Lesson 12 knowledge!

fn main() {
    let roll = rand::thread_rng().gen_range(1..=6);
    println!("🎲 you rolled {roll}");
}

Names worth knowing for later: serde (JSON), clap (command-line interfaces), tokio (async), axum (web servers), bevy (games).

The professional polish kit 💅

CommandWhat it does
cargo fmtformats your code the standard way; ends all style debates
cargo clippya lint sage: flags un-idiomatic code and teaches the better way
cargo doc --opengenerates a documentation website from your /// comments
cargo build --releasefull-speed optimized binary (debug builds are slow on purpose)

Run fmt and clippy before every commit and your code will look senior from day one. Clippy in particular is a free mentor: it explains why.

Graduation 🎓🦀

Look at your inventory: variables, functions, ownership, borrowing, structs, enums, pattern matching, collections, error handling, traits, generics, lifetimes, iterators, smart pointers, threads, modules, tests. That's not "beginner Rust." That's Rust. Where to go from here:

Final Exercise

Ship a real program

Build a tip-splitting CLI: a calc module with total_with_tip and split_between functions (both with tests!), and a main that reads the bill from std::env::args() and prints each person's share. Run cargo fmt, cargo clippy, and cargo test before you call it done.

Reveal one possible solution
// src/main.rs
mod calc;

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let bill: f64 = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(100.0);
    let people: u32 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(2);

    let total = calc::total_with_tip(bill, 18.0);
    let share = calc::split_between(total, people);
    println!("total {total:.2}, {people} people → {share:.2} each");
}
// src/calc.rs
pub fn total_with_tip(bill: f64, tip_percent: f64) -> f64 {
    bill + bill * tip_percent / 100.0
}

pub fn split_between(total: f64, people: u32) -> f64 {
    total / people.max(1) as f64   // .max(1): no dividing by zero
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tip_is_added() {
        assert_eq!(total_with_tip(100.0, 18.0), 118.0);
    }

    #[test]
    fn zero_people_doesnt_explode() {
        assert_eq!(split_between(100.0, 0), 100.0);
    }
}

Run it: cargo run -- 84.50 3. Notice the test for the zero-people edge case: thinking of those is what "experienced" means. 🧠

🎉 You did it! All 17 lessons. Take the Power Tools quiz to make it official, then go build something. Welcome to the Rustacean life. Rusty is proud of you. 🦀🧡