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 💅
| Command | What it does |
|---|---|
cargo fmt | formats your code the standard way; ends all style debates |
cargo clippy | a lint sage: flags un-idiomatic code and teaches the better way |
cargo doc --open | generates a documentation website from your /// comments |
cargo build --release | full-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:
- Read this site's own server:
src/main.rsin this project is a real threaded web server in ~150 lines of pure std, and you now know every concept in it. Best possible capstone read. - Build something small you'd use: a todo CLI, a dice game, a file renamer. Finished beats perfect.
- Rustlings: dozens of drill exercises to sharpen instincts.
- The Rust Book: the official deep-dive; after this course it will read easily.
- Join the crab pile: users.rust-lang.org is famously the friendliest forum in programming. Ask anything.
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. 🧠