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_orderNote: 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.rssrc/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.
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.rssrc/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.rsWriting Tests
Rust has excellent built-in testing support. Tests are functions marked with the #[test] attribute.
Basic Test Structure
Run tests with:
cargo testThe
#[cfg(test)]annotation tells Rust to compile and run the test code only when runningcargo 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.rssrc/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 testOr run only integration tests:
cargo test --test integration_testIntegration 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:
cargo test --docThe 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 testtest src/lib.rs - add (line 3) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25sGenerate documentation with:
cargo doc --openNote: 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
modkeyword - 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 formenu.rs - Nested modules can use directories with a
.rsfile ormod.rs - Binary and library crates live in
src/main.rsandsrc/lib.rs - Dependencies are added in
Cargo.tomland 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:
-
Create a new Rust project using
cargo new project_with_tests. -
Organize your code into modules:
- Create a
src/lib.rsfile 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 inlib.rsusing themodkeyword.
- Create a
-
Implement functionality in your modules. For example, you might create functions for basic arithmetic operations, string manipulations, or data processing.
-
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.
- Add
-
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.
- Create a
-
Run your tests using
cargo testto 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.