← Table of contents

3.7 The Proof Moves In

A test that lives separately from what it tests is still a test. This is usually called organisation. The accurate word is accidental proximity.

What You Need

Fourteen tests live in src/main.rs - because when they were written, there were no other files. Now there are. A task test should live next to the task; a store test next to the store.

There is also a practical reason: a test as a submodule sees the private details of its parent. A test living in main.rs sees only what is exposed to the outside. A test living inside its module sees everything.

The Move

In chapter 3.6, the task module is the file src/task.rs. To give it a submodule with tests, the file becomes a directory: src/task.rs is renamed to src/task/mod.rs. For the compiler and main.rs, nothing changes - mod task; works the same way with both.

Add one line at the end of src/task/mod.rs:

// src/task/mod.rs - add at the end
#[cfg(test)]
mod tests;

#[cfg(test)] is a conditional compilation attribute: the tests module is included only when running cargo test. mod tests; points the compiler to src/task/tests.rs.

Create src/task/tests.rs. Inside - the body of the tests module:

// src/task/tests.rs - NEW
use super::*;

#[test]
fn new_task_starts_as_todo() {
    let task = Task::new(1, "Buy coffee").unwrap();
    assert!(!task.is_done());
}

#[test]
fn task_display_todo() {
    let task = Task::new(1, "Buy coffee").unwrap();
    assert_eq!(format!("{}", task), "#1: Buy coffee [Todo]");
}

The remaining seven task tests move there by the same principle - each unchanged.

super here is the task module. use super::* brings all names from task/mod.rs into scope - both public and private: a child module sees everything the parent has. If task/mod.rs already has use std::fmt or use crate::error::TqError, those are available in tests.rs without repeating the import. That is exactly why tests live inside, not outside.

src/store.rs and src/error.rs are renamed the same way - to src/store/mod.rs and src/error/mod.rs. Each gets #[cfg(test)] mod tests; at the end.

src/store/tests.rs needs one extra import: TqError lives in a different module and use super::* will not bring it:

// src/store/tests.rs - NEW
use super::*;
use crate::error::TqError;

#[test]
fn store_assigns_sequential_ids() {
    let mut store = TaskStore::new();
    store.add("A").unwrap();
    store.add("B").unwrap();
    assert_eq!(store.all()[0].id, 1);
    assert_eq!(store.all()[1].id, 2);
}

The remaining two store tests move there as well.

The entire #[cfg(test)] mod tests { ... } block is deleted from src/main.rs.

The Result

running 14 tests
test error::tests::error_display_empty_title ... ok
test error::tests::error_display_not_found ... ok
test store::tests::store_assigns_sequential_ids ... ok
test store::tests::store_get_mut_completes_task ... ok
test store::tests::store_get_returns_not_found ... ok
test task::tests::complete_marks_task_done ... ok
test task::tests::new_task_accepts_valid_title ... ok
test task::tests::new_task_rejects_empty_title ... ok
test task::tests::new_task_starts_as_todo ... ok
test task::tests::str_converts_into_task ... ok
test task::tests::task_display_done ... ok
test task::tests::task_display_todo ... ok
test task::tests::task_from_str_sets_title ... ok
test task::tests::task_id_is_preserved ... ok

test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Fourteen tests - the same ones as before. Only now each one knows where it lives.

make ci passes.

The complete tq code for this chapter is in 3-a-voice/07-the-proof-moves-in/.