Learning Modular Monolith Architecture with Rust
An 7-project progression from Hello World to a fully decoupled, I/O-agnostic application using traits and crates
This is Episode 02
All the examples are on GitHub
The Posts Of The Saga
- Episode 00: Introduction + Step 00 - First prototype working
- Episode 01: Step 01 - Split the source code in multiple files
- Episode 02: Step 02 - Add a test folder
- Episode 03: Step 03 - Implement Hexagonal Architecture
- Episode 04: Step 04 - One crate per component
- Episode 05: Step 05 - Anyhow & ThisError
- Episode 06: Step 06 - Add new adapters + Conclusion

Table of Contents
Objective
We want to create a tests/ folder to host the integration tests and the domain tests (since greet() is public). At the end, the project folder will look like this:
step_02/
│ Cargo.toml
├───src
│ domain.rs # Business rules (isolated)
│ error.rs # Error and Result type alias
│ lib.rs # Library re-exports
│ main.rs # Entry point + console I/O
└───tests
domain_test.rs # Domain unit tests
integration_test.rs # Integration tests
Setup
- Save your work
- Quit VSCode
- You should have a terminal open and you should be in the
step_01/folder
cd ..
# make a copy the folder step_01 and name it step_02
Copy-Item ./step_01 ./step_02 -Recurse
cd step_02
code .
Actions
Cargo.toml
[package]
name = "step_02"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "step_02"
path = "src/main.rs
lib.rs
pub mod domain;
pub mod error;
// DO NOT re-export greet() for convenience
// I want to write domain::greet() in main.rs
// pub use domain::greet;
Points of attention:
- I no longer re-export
greet()from thedomainmodule. I want to have to writedomain::greet(). This will help me to read the code in 6 months. Inmain.rsI writeuse step_02::domain;then I calldomain::greet(name).
domain_test.rs
Create a tests/ folder with domain_test.rs and integration_test.rs. Move the tests from domain.rs to domain_test.rs.
use step_02::domain::greet;
const MAX_LENGTH: usize = 25;
const TRAILER: &str = "...";
#[test]
fn empty_name_returns_error() {
let result = greet("");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.to_string(), "Name cannot be empty");
}
// the others tests
Points of attention:
- We are testing
domain. So at the top of the file there isuse step_02::domain::greet;and we callgreet()in the rest of the code. - This is Ok for me because no confusion is possible.
integration_test.rs
Write the integration_test.rs file
use step_02::domain;
const MAX_LENGTH: usize = 25;
const TRAILER: &str = "...";
#[test]
fn greet_integration() {
let result = domain::greet("World");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Hello World.");
}
#[test]
fn roberto_integration() {
let result = domain::greet("Roberto");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Ciao Roberto!");
}
#[test]
fn empty_name_integration() {
let result = domain::greet("");
assert!(result.is_err());
}
#[test]
fn long_name_integration() {
let result = domain::greet("VeryLongNameThatWillBeTruncated");
assert!(result.is_ok());
let greeting = result.unwrap();
assert_eq!(greeting.len(), MAX_LENGTH);
assert!(greeting.ends_with(TRAILER));
}
Points of attention:
- At this point
domain_test.rsandintegration_test.rslook very similar. This is because our project have only one component (domain.rs). - Later, at the top of the
integration_test.rswe will have multipleuse step_NN::component;lines. - At the top of the file there is a
use step_02::domain;and in the code we calldomain::greet()). This is Ok for me because some confusion may happen when more modules areuse.
Build, run & test
Build, run and test the application. Find below the expected output:
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `C:/Users/phili/rust_builds/Documents/Programmation/rust/01_xp/046_modular_monolith/step_02\debug\step_02.exe`
=== Greeting Service (Step 01) ===
Enter a name to greet (or 'quit' to exit):
> Buck
Hello Buck.
> Quit
Goodbye!
cargo test
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s
Running unittests src\lib.rs (C:/Users/phili/rust_builds/Documents/Programmation/rust/01_xp/046_modular_monolith/step_02\debug\deps\step_02-21333d23e061cf65.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src\main.rs (C:/Users/phili/rust_builds/Documents/Programmation/rust/01_xp/046_modular_monolith/step_02\debug\deps\step_02-573b6b6ff085e89c.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests\domain_test.rs (C:/Users/phili/rust_builds/Documents/Programmation/rust/01_xp/046_modular_monolith/step_02\debug\deps\domain_test-3722a907fedce176.exe)
running 9 tests
test boundary_case_nineteen_chars ... ok
test domain_should_not_use_special_greeting_for_similar_names ... ok
test domain_should_handle_unicode_names ... ok
test empty_name_returns_error ... ok
test domain_should_truncate_long_unicode_names ... ok
test normal_greeting ... ok
test roberto_special_case ... ok
test truncation_for_long_names ... ok
test greeting_length_limit ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests\integration_test.rs (C:/Users/phili/rust_builds/Documents/Programmation/rust/01_xp/046_modular_monolith/step_02\debug\deps\integration_test-c2e846bff70f532b.exe)
running 4 tests
test empty_name_integration ... ok
test long_name_integration ... ok
test roberto_integration ... ok
test greet_integration ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests step_02
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Summary
What have we done so far?
- Nothing change from the outside (which is good)
domains.rsis shorter- The tests are at the right place
- We now have a set of integration tests
- So far, so good…
Next Steps
- Episode 00: Introduction + Step 00 - First prototype working
- Episode 01: Step 01 - Split the source code in multiple files
- Episode 02: Step 02 - Add a test folder
- Episode 03: Step 03 - Implement Hexagonal Architecture
- Episode 04: Step 04 - One crate per component
- Episode 05: Step 05 - Anyhow & ThisError
- Episode 06: Step 06 - Add new adapters + Conclusion