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.

Default Visibility

0 / 3 points

What is the default visibility of items in Rust modules?

The use Keyword

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

You can also use as to rename imports:


The use Keyword

0 / 3 points

What does the use keyword do?

use front_of_house::hosting;
fn main() {
hosting::add_to_waitlist();
}

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");
}

File and Module Correspondence

0 / 3 points

Given this file structure, how do you declare the modules in main.rs?

src/
├── main.rs
├── menu.rs
└── orders.rs

Writing Tests

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

Basic Test Structure

Run tests with:

Terminal window
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.

Test Module Annotation

0 / 3 points

What does #[cfg(test)] do above a test module?

The Assert Macros

Rust provides several assert macros:


Assert Macros

0 / 3 points

Which statements about assert macros are true?

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.

Testing with Result

0 / 3 points

What advantage does returning Result<(), E> from tests provide?

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:


Unit Tests Location

0 / 3 points

Where should unit tests be placed?

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:

Terminal window
cargo test

Or run only integration tests:

Terminal window
cargo test --test integration_test

Integration Tests

0 / 3 points

Which statements about integration tests are true?

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.

Common Test Utilities

0 / 3 points

How do you create shared test utilities for integration tests?


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:

Terminal window
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:

Terminal window
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

Project with Multiple Files and Tests

0 / 40 points

Good organization and testing help make maintainable Rust projects. The compiler helps you write correct code, but tests help you verify it does what you intend.

In this assignment, you’ll create a Rust project that uses multiple files (modules) and includes unit and integration tests. Follow these steps:

  1. Create a new Rust project using cargo new project_with_tests.

  2. Organize your code into modules:

    • Create a src/lib.rs file to define your library’s public API.
    • Create additional module files in the src/ directory (e.g., src/module1.rs, src/module2.rs) and declare them in lib.rs using the mod keyword.
  3. Implement functionality in your modules. For example, you might create functions for basic arithmetic operations, string manipulations, or data processing.

  4. Write unit tests for each module:

    • Add #[cfg(test)] modules in each of your module files.
    • Write test functions using the #[test] attribute to verify the correctness of your functions.
  5. Create integration tests:

    • Create a tests/ directory in the root of your project.
    • Add integration test files (e.g., tests/integration_test.rs) that use your library’s public API to test its functionality in a broader context.
  6. Run your tests using cargo test to ensure everything works as expected.

Submit your project directory, including all source files and tests, for evaluation. As the assignment is open-ended, there are no automated tests.