Rust Fundamentals

Modules and Testing


Learning Objectives

  • You can organize code into modules for better structure.
  • You understand the relationship between files and modules.
  • You know how to control visibility with pub.
  • You can use external crates as dependencies.
  • You can write and run unit tests for your code.
  • You understand integration tests and when to use them.
  • You can write documentation tests that stay up-to-date.

The Module System

As programs grow, organizing code becomes important. Rust’s module system helps you split code into logical units, manage visibility, and keep related functionality together.

Creating Modules

Modules are created with the mod keyword:

This creates a module tree:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         └── serve_order

Note: Everything in Rust is private by default. Parent modules cannot access private items in child modules, and child modules cannot access private items in parent modules.

Public vs Private

Use the pub keyword to make items public:

Both the module (pub mod hosting) and the function (pub fn add_to_waitlist) need to be public.

Loading Exercise...

The use Keyword

Typing full paths gets tedious. The use keyword brings items into scope:

You can also use as to rename imports:


Loading Exercise...

Organizing Code Across Files

As your program grows, you’ll want to split modules into separate files.

File Structure and Modules

Let’s organize our coffee shop code. Here’s the structure we want:

coffee_shop/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── menu.rs
    ├── orders.rs
    └── payments.rs

src/main.rs:

mod menu;
mod orders;
mod payments;

fn main() {
  println!("Welcome to Rust Coffee Shop!");
  menu::display_menu();

  let order = orders::create_order("Latte");
  orders::process_order(&order);

  payments::process_payment(4.00);
}

src/menu.rs:

pub fn display_menu() {
  println!("=== Menu ===");
  println!("1. Espresso - 2.50 euros");
  println!("2. Latte - 4.00 euros");
  println!("3. Cappuccino - 3.50 euros");
}

pub fn get_price(item: &str) -> Option<f64> {
  match item {
    "Espresso" => Some(2.50),
    "Latte" => Some(4.00),
    "Cappuccino" => Some(3.50),
    _ => None,
  }
}

src/orders.rs:

pub struct Order {
  item: String,
  quantity: u32,
}

pub fn create_order(item: &str) -> Order {
  Order {
    item: String::from(item),
    quantity: 1,
  }
}

pub fn process_order(order: &Order) {
  println!("Processing order: {} x{}", order.item, order.quantity);
}

src/payments.rs:

pub fn process_payment(amount: f64) {
  println!("Processing payment of {:.2} euros", amount);
  println!("Payment successful!");
}

The mod keyword in main.rs tells Rust to look for those modules in separate files.

Library files

If you’re creating a library crate, the main file is src/lib.rs instead of src/main.rs. The module structure works the same way.

Nested Modules in Files

You can also create subdirectories for nested modules.

src/
├── main.rs
├── front_of_house.rs
└── front_of_house/
    ├── hosting.rs
    └── serving.rs

src/front_of_house.rs:

pub mod hosting;
pub mod serving;

src/front_of_house/hosting.rs:

pub fn add_to_waitlist() {
  println!("Adding to waitlist");
}

src/front_of_house/serving.rs:

pub fn take_order() {
  println!("Taking order");
}
Loading Exercise...

Writing Tests

Rust has excellent built-in testing support. Tests are functions marked with the #[test] attribute.

Basic Test Structure

Run tests with:

cargo test

The #[cfg(test)] annotation tells Rust to compile and run the test code only when running cargo test, not when building the regular binary.

Loading Exercise...

The Assert Macros

Rust provides several assert macros:


Loading Exercise...

Testing for Panics

Sometimes you want to verify that code panics in certain situations:

Testing with Result

Tests can return Result<(), E> instead of panicking:

Using Result in tests is convenient because you can use the ? operator. The test fails if the function returns Err.

Loading Exercise...

Unit Tests

Unit tests test individual units of code in isolation. They typically live in the same file as the code they test, inside a tests module.

Unit tests can test private functions because they’re in the same module:


Loading Exercise...

Integration Tests

Integration tests test your library from the outside, as if you were a user. They go in a separate tests directory.

coffee_shop/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   └── main.rs
└── tests/
    ├── integration_test.rs
    └── common/
        └── mod.rs

src/lib.rs:

pub fn create_order(item: &str, quantity: i32) -> String {
    format!("Order: {} x{}", item, quantity)
}

pub fn calculate_total(items: &[(f64, i32)]) -> f64 {
    items.iter()
        .map(|(price, quantity)| price * *quantity as f64)
        .sum()
}

pub struct Menu;

impl Menu {
    pub fn get_price(item: &str) -> Option<f64> {
        match item {
            "Espresso" => Some(2.50),
            "Latte" => Some(4.00),
            "Cappuccino" => Some(3.50),
            _ => None,
        }
    }
}

tests/integration_test.rs:

use coffee_shop::{create_order, calculate_total, Menu};

#[test]
fn test_create_order() {
    let order = create_order("Latte", 2);
    assert_eq!(order, "Order: Latte x2");
}

#[test]
fn test_calculate_total() {
    let items = vec![
        (2.50, 2),  // 2 Espressos
        (4.00, 1),  // 1 Latte
    ];

    let total = calculate_total(&items);
    assert_eq!(total, 9.00);
}

#[test]
fn test_menu_known_item() {
    assert_eq!(Menu::get_price("Espresso"), Some(2.50));
}

#[test]
fn test_menu_unknown_item() {
    assert_eq!(Menu::get_price("Tea"), None);
}

#[test]
fn test_complete_order_flow() {
    // Test the complete flow
    let item = "Latte";
    let quantity = 3;

    // Get price from menu
    let price = Menu::get_price(item).expect("Item should be on menu");

    // Create order
    let order = create_order(item, quantity);
    assert!(order.contains("Latte"));
    assert!(order.contains("3"));

    // Calculate total
    let total = calculate_total(&[(price, quantity)]);
    assert_eq!(total, 12.00);
}

Run integration tests with:

cargo test

Or run only integration tests:

cargo test --test integration_test
Loading Exercise...

Common Test Utilities

You can create shared test utilities in tests/common/:

tests/common/mod.rs:

pub fn setup_test_menu() -> Vec<(&'static str, f64)> {
    vec![
        ("Espresso", 2.50),
        ("Latte", 4.00),
        ("Cappuccino", 3.50),
    ]
}

pub fn assert_price_in_range(price: f64, min: f64, max: f64) {
    assert!(
        price >= min && price <= max,
        "Price {} not in range {}-{}",
        price, min, max
    );
}

tests/integration_test.rs:

mod common;

use coffee_shop::Menu;

#[test]
fn test_all_prices_reasonable() {
    let menu = common::setup_test_menu();

    for (item, expected_price) in menu {
        let price = Menu::get_price(item).expect("Item should exist");
        assert_eq!(price, expected_price);
        common::assert_price_in_range(price, 1.0, 10.0);
    }
}

Note: Files in tests/common/ are not treated as integration test files because they’re in a subdirectory. This is the convention for shared test code.

Loading Exercise...

Documentation tests

Rust can also run code examples in your documentation comments as tests. This ensures your examples stay up-to-date and correct. For example, if your project is called aalto_rust_opencs, you can write a public library function (to lib.rs) with a testable doc comment like this:

/// Adds two numbers together.
/// # Example
/// ```
/// use aalto_rust_opencs::add;
/// let sum = add(2, 3);
/// assert_eq!(sum, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Run documentation tests with:

cargo test --doc

The output would be similar to the following:

cargo test --doc
   Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s
   Doc-tests aalto_rust_opencs

running 1 test
test src/lib.rs - add (line 3) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s

Generate documentation with:

cargo doc --open

Note: Documentation tests are compiled as separate test programs, so they need to include the necessary imports. For library crates, examples automatically have access to the public API.

Summary

In this chapter, we learned how to organize and test Rust projects:

  • Modules organize code with the mod keyword
  • Visibility is controlled with pub - everything is private by default
  • The use keyword brings items into scope
  • pub use re-exports items for a cleaner public API
  • Files and modules correspond - mod menu; looks for menu.rs
  • Nested modules can use directories with a .rs file or mod.rs
  • Binary and library crates live in src/main.rs and src/lib.rs
  • Dependencies are added in Cargo.toml and come from crates.io
  • Unit tests live alongside code in #[cfg(test)] modules
  • Integration tests go in the tests/ directory
  • Documentation tests in doc comments ensure examples stay current
  • cargo test runs all tests by default
Loading Exercise...