Modules, Crates, Testing, and Documentation
Organize code with modules, create reusable crates, write tests with the built-in framework, and generate documentation. Learn to publish crates to crates.io with confidence.
Code grows. A single file becomes unwieldy. Functions need organization. Related types group together. Implementation details hide from external code. Rust's module system organizes code into namespaces. Crates package code for reuse. Testing verifies correctness. Documentation explains intent. Together, these features support building and maintaining large codebases.
The Rust ecosystem thrives on shareable, well-tested, documented libraries. When you publish code, you're not just shipping functionality—you're shipping confidence that it works and clarity about how to use it.
Modules
Modules organize code into namespaces. Define a module with the mod keyword:
mod network {
fn connect() {
println!("Connecting...");
}
}
The function connect lives in the network module. Access it with network::connect().
By default, items in modules are private. Make them public with pub:
mod network {
pub fn connect() {
println!("Connecting...");
}
}
fn main() {
network::connect();
}
Nested modules create hierarchies:
mod network {
pub mod client {
pub fn connect() {
println!("Client connecting...");
}
}
pub mod server {
pub fn start() {
println!("Server starting...");
}
}
}
fn main() {
network::client::connect();
network::server::start();
}
Multiple Files
Modules can live in separate files. Create a file named after the module:
src/
├── main.rs
└── network.rs
In main.rs:
mod network;
fn main() {
network::connect();
}
In network.rs:
pub fn connect() {
println!("Connecting...");
}
The declaration mod network; tells the compiler to load the contents of network.rs.
For modules with submodules, use a directory:
src/
├── main.rs
└── network/
├── mod.rs
├── client.rs
└── server.rs
In network/mod.rs:
pub mod client;
pub mod server;
In network/client.rs:
pub fn connect() {
println!("Client connecting...");
}
The mod.rs file declares the module's submodules. This pattern scales to arbitrary nesting.
The use Keyword
Bring items into scope with use:
mod network {
pub mod client {
pub fn connect() {}
}
}
use network::client;
fn main() {
client::connect();
}
Bring specific items:
use network::client::connect;
fn main() {
connect();
}
The as keyword creates aliases:
use network::client::connect as client_connect;
fn main() {
client_connect();
}
Re-export items with pub use:
mod network {
pub mod client {
pub fn connect() {}
}
}
pub use network::client;
Code outside this module can access client directly without navigating through network.
Crates
A crate is a compilation unit—a library or binary. Binary crates compile to executables. Library crates compile to reusable libraries.
Cargo manages crates. The Cargo.toml file specifies metadata and dependencies:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0"
tokio = { version = "1.0", features = ["full"] }
Run cargo build to download dependencies, compile them, and link them with your code.
Use dependencies with use:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
The crate serde provides serialization functionality. Cargo downloads it from crates.io, compiles it, and makes it available.
Publishing Crates
Publish crates to crates.io to share with others. Add documentation and metadata to Cargo.toml:
[package]
name = "my_library"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]
edition = "2021"
description = "A short description"
license = "MIT"
repository = "https://github.com/yourusername/my_library"
Document your code with doc comments:
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_library::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Run cargo doc --open to generate and view documentation. Run cargo publish to upload to crates.io.
Testing
Rust includes a built-in testing framework. Write tests as functions annotated with #[test]:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-2, 3), 1);
}
}
The #[cfg(test)] attribute compiles the module only when running tests. This prevents test code from bloating the binary.
Run tests with cargo test:
running 2 tests
test tests::test_add ... ok
test tests::test_add_negative ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Tests panic to indicate failure. Use assertion macros:
assert_eq!(actual, expected);
assert_ne!(actual, not_expected);
assert!(condition);
Custom failure messages:
assert_eq!(add(2, 3), 5, "Expected 2 + 3 to equal 5");
Test Organization
Unit tests live in the same file as the code they test. Integration tests live in the tests/ directory:
src/
├── lib.rs
tests/
├── integration_test.rs
In tests/integration_test.rs:
use my_library;
#[test]
fn test_public_api() {
assert_eq!(my_library::add(2, 3), 5);
}
Integration tests treat your crate as an external user would. They import it like any other dependency and test its public API.
Testing Error Cases
Test that code panics with #[should_panic]:
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero");
}
a / b
}
#[test]
#[should_panic(expected = "Division by zero")]
fn test_divide_by_zero() {
divide(10, 0);
}
The test passes if the code panics with the expected message.
Test functions returning Result:
#[test]
fn test_parse() -> Result<(), ParseIntError> {
let number: i32 = "10".parse()?;
assert_eq!(number, 10);
Ok(())
}
The test fails if the function returns Err.
Documentation Tests
Documentation examples are automatically tested. Code blocks in doc comments become tests:
/// Adds two numbers.
///
/// # Examples
///
/// ```
/// let result = my_library::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Run cargo test to execute documentation tests. This ensures examples stay correct as code evolves.
Mark code blocks that shouldn't run with no_run:
/// ```no_run
/// let server = start_server();
/// ```
Mark code blocks that should fail compilation:
/// ```compile_fail
/// let x: i32 = "not a number";
/// ```
Documentation Comments
Use /// for item documentation:
/// Represents a user in the system.
pub struct User {
/// The user's name.
pub name: String,
}
Use //! for module-level documentation at the top of files:
//! This module provides network utilities.
//!
//! It includes client and server implementations.
pub mod client;
pub mod server;
Documentation supports Markdown:
/// Computes the **area** of a circle.
///
/// # Arguments
///
/// * `radius` - The circle's radius
///
/// # Returns
///
/// The area as a `f64`
pub fn circle_area(radius: f64) -> f64 {
std::f64::consts::PI * radius * radius
}
Conditional Compilation
Compile code conditionally with attributes:
#[cfg(target_os = "linux")]
fn platform_specific() {
println!("Running on Linux");
}
#[cfg(target_os = "windows")]
fn platform_specific() {
println!("Running on Windows");
}
The cfg attribute includes code only when conditions are met. Use this for platform-specific code, feature flags, or debug builds.
Feature Flags
Define optional features in Cargo.toml:
[features]
default = ["standard"]
standard = []
advanced = []
Enable features conditionally:
#[cfg(feature = "advanced")]
pub fn advanced_feature() {
println!("Advanced feature enabled");
}
Users enable features when depending on your crate:
[dependencies]
my_library = { version = "0.1", features = ["advanced"] }
This lets you provide optional functionality without bloating the default build.
The Rust Ecosystem
The crates.io registry hosts over 130,000 crates. Common categories include web frameworks (Actix, Axum, Rocket), async runtimes (Tokio, async-std), serialization (Serde), databases (SQLx, Diesel), and command-line tools (clap, structopt).
Dependencies integrate seamlessly. Cargo handles versioning, compilation, and linking. Add a crate to Cargo.toml and start using it.
Well-maintained crates provide comprehensive tests and documentation. The ecosystem values correctness and clarity. When you publish a crate, include tests proving it works and documentation explaining how to use it.
Why Organization and Testing Matter
Modules prevent namespace pollution. Related code groups together. Implementation details hide behind public interfaces. Refactoring becomes safer—changes to private code don't affect external users.
Testing catches regressions. When you change code, tests verify it still works. Rust's testing framework integrates with the language and build system. Writing tests requires no external tools. Running tests is a single command.
Documentation serves as both explanation and verification. Examples show how code works. Documentation tests ensure examples stay correct. When your documentation compiles, it's accurate.
Together, modules, crates, testing, and documentation support building reliable software. Organize code clearly. Test thoroughly. Document comprehensively. When you ship code, you ship confidence. Welcome to the Rust ecosystem—where code either compiles correctly or doesn't compile at all, and where testing and documentation are first-class citizens.
Previous
Collections and Iterators
Next
No next page
