Unit Tests
Unit tests are typically placed in the same file as the code they test, in a module annotated with #[cfg(test)]
:
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
// Bring the parent module's items into scope
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
Integration Tests
Integration tests are placed in a separate tests
directory at the same level as src
:
project/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
// tests/integration_test.rs
use my_crate; // The crate being tested
#[test]
fn test_add_integration() {
assert_eq!(my_crate::add(2, 3), 5);
}
Best Practices for Module Organization
Based on experience from large Rust projects, here are some best practices:
1. Follow the Principle of Least Visibility
Make items as private as possible:
mod database {
// Private implementation details
const CONNECTION_TIMEOUT: u32 = 30;
fn establish_connection() -> Connection {
// Implementation
}
// Public API
pub struct Connection {
// Fields are private
handle: u64,
}
impl Connection {
pub fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
// Implementation
}
}
}
2. Create a Clear Public API
Use re-exports to create a clean public interface:
// lib.rs
mod config;
mod database;
mod api;
mod utils;
// Public API
pub use config::Config;
pub use database::Connection;
pub use api::{Client, Response};
// Internal module structure remains hidden
3. Group Related Functionality
Organize related functionality into modules:
mod user {
pub struct User {
// Fields
}
pub fn create_user() -> User {
// Implementation
}
pub fn authenticate_user() -> bool {
// Implementation
}
}
mod product {
pub struct Product {
// Fields
}
pub fn list_products() -> Vec<Product> {
// Implementation
}
}
4. Use Submodules for Implementation Details
Hide implementation details in submodules:
pub mod http {
// Public API
pub struct Client {
// Fields
}
impl Client {
pub fn new() -> Client {
let config = self::config::default_config();
// Implementation
}
pub fn get(&self, url: &str) -> Response {
// Implementation
}
}
pub struct Response {
// Fields
}
// Private implementation details
mod config {
pub(super) fn default_config() -> Config {
// Implementation
}
pub(super) struct Config {
// Fields
}
}
}
5. Avoid Deep Module Hierarchies
Keep your module hierarchy relatively flat:
// Instead of:
mod a {
mod b {
mod c {
mod d {
pub fn function() {
// Implementation
}
}
}
}
}
// Prefer:
mod a_b {
pub fn function1() {
// Implementation
}
}
mod a_c {
pub fn function2() {
// Implementation
}
}
Real-World Example: A Complete Project Structure
Let’s look at a complete example of a well-structured Rust project:
my_project/
├── Cargo.toml
├── src/
│ ├── main.rs # Application entry point
│ ├── lib.rs # Library entry point and public API
│ ├── config/ # Configuration handling
│ │ ├── mod.rs
│ │ └── types.rs
│ ├── api/ # API client
│ │ ├── mod.rs
│ │ ├── client.rs
│ │ └── response.rs
│ ├── database/ # Database interaction
│ │ ├── mod.rs
│ │ ├── models.rs
│ │ └── schema.rs
│ └── utils/ # Utility functions
│ ├── mod.rs
│ └── helpers.rs
└── tests/ # Integration tests
├── api_tests.rs
└── database_tests.rs
// src/lib.rs
pub mod config;
pub mod api;
mod database; // Not public
mod utils; // Not public
// Re-export public API
pub use config::Config;
pub use api::{Client, Response};
// Public functions that use private modules
pub fn initialize() -> Result<(), Error> {
let db_config = database::get_config();
database::initialize(db_config)?;
Ok(())
}
pub fn get_data(query: &str) -> Result<Vec<String>, Error> {
let connection = database::connect()?;
let results = database::query(&connection, query)?;
Ok(results)
}
// src/main.rs
use my_project::{Config, Client, initialize, get_data};
fn main() -> Result<(), Box<dyn std::error::Error>> {
initialize()?;
let config = Config::new("config.json")?;
let client = Client::new(config);
let response = client.send_request("/api/data");
println!("Response: {:?}", response);
let data = get_data("SELECT * FROM items")?;
println!("Data: {:?}", data);
Ok(())
}
Conclusion
Rust’s module system is a powerful tool for organizing code, controlling visibility, and creating clear boundaries between components. By understanding how modules work and following best practices, you can create well-structured, maintainable projects that are easy to understand and extend.
Key takeaways from this guide include:
- Use modules to organize related code into logical units
- Control visibility with the
pub
keyword and other visibility modifiers - Structure your code across files and directories as your project grows
- Create a clear public API using re-exports
- Follow the principle of least visibility to encapsulate implementation details
- Use the module system to enforce boundaries between components
As you continue your journey with Rust, you’ll find that a well-organized module structure becomes increasingly important as your projects grow in size and complexity. The time invested in thoughtfully structuring your code will pay dividends in maintainability, readability, and collaboration with other developers.
Remember that the module system is not just about organization—it’s about communication. A well-structured codebase communicates its design and intentions to other developers (including your future self), making it easier to understand, modify, and extend. By mastering Rust’s module system, you’re taking a significant step toward writing better, more maintainable code.