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::prelude::*;   // trait imports, Lesson 12 knowledge!

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

Older tutorials write rand::thread_rng() and .gen_range(); those names were renamed in 2025. When a tutorial and the compiler disagree, trust docs.rs.

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! The full core path, all 17 lessons. Take the Power Tools quiz to make it official, then go build something. And when you are hungry for more, Level 4: Deep Cuts is waiting: strings for real, pattern mastery, professional error handling, async, unsafe, and performance. Welcome to the Rustacean life. Rusty is proud of you. 🦀🧡