4e433158cd
---ci--- project: atelier phase: 3 milestone: v0.4 status: complete phase_role: execution phase_tag: v0.3.3 requirements: covered: [ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105] partial: [] ---/ci---
7.2 KiB
7.2 KiB
Rust Testing — Derived Application
Applies Atelier's domain principles to Rust testing specifically. Derives from
domains/docs; introduces no new P-rules (D-063). Seelanguages/rust.mdfor the language first-principles stub.
#[test] and Co-located Tests (Testing P1 Tests as Specification, C2 Clarity)
#[test]on functions in a#[cfg(test)] mod testsblock: tests co-located with source, compiled only incargo test. A test file far from its subject rots (Documentation P5 Discoverability).- Test names read as a spec:
fn create_user_rejects_invalid_email()— a reader understands the unit from the name. Avoidfn test_user_1(). assert!/assert_eq!/assert_ne!over rawpanic!: the macros produce readable failure output (assertion failed: left == right, left: 5, right: 3). Rawpanic!gives a message only (Testing P6 Failure Specificity).- Applies
Testing P1: the test is a specification; the failure message is the spec violation.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_user_rejects_invalid_email() {
let r = create_user("not-an-email");
assert!(matches!(r, Err(Error::Validation(_))));
}
#[test]
fn create_user_returns_persisted_id() {
let u = create_user("a@b.co").unwrap();
assert!(!u.id.is_empty());
}
}
proptest and Property Tests (Testing P9 Edge Case Coverage, Testing P1 Tests as Specification)
proptest(orquickcheck) for invariant tests: declare a property (parse(serialize(x)) == x), the framework generates hundreds of inputs and shrinks failures to a minimal counterexample (Testing P9).- Strategy over hand-written generators:
proptest::collection::vec(any::<u32>(), 0..100)generates arbitraryVec<u32>; do not hand-roll a generator for each property. proptest!macro orproptest! { ... }block: eachcase (name) => { ... }is a property. The block is the spec (Testing P1).- Property tests complement, not replace, example tests: examples document the happy path; properties cover the edge space. Both are required.
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrips_id(s in "[a-z0-9]{1,32}") {
let id = UserId::new(&s).unwrap();
assert_eq!(id.as_str(), s);
}
#[test]
fn rejects_invalid_id(s in "[^a-z0-9]+") {
assert!(UserId::new(&s).is_err());
}
}
Mock Discipline (Testing P2 Independence, Testing P7 Realism)
- Mock at the trait, not the struct:
trait Store { fn get(&self, id: &str) -> Result<User, Error>; }in production;#[automock] trait Store(viamockall) in test. The trait is the contract. mockallfor generated mocks:#[automock] trait Repo {}generatesMockRepowithexpect_*methods. Each expectation is per-test; no shared mock state (Testing P2 Independence).- Mock the boundary, not the unit: mock
Repo, notUserService(the unit). Mocking the unit under test tests the mock (Testing P7 realism). - No
#[cfg(test)]on production code paths to inject mocks: instead, accept the trait as a generic ordynparameter. Test-only branches in production code are dead code in prod.
use mockall::*;
#[automock]
trait UserRepo {
fn get(&self, id: &str) -> Result<User, Error>;
}
#[test]
fn get_user_returns_not_found() {
let mut repo = MockUserRepo::new();
repo.expect_get()
.with(eq("abc"))
.returning(|_| Err(Error::NotFound));
let svc = UserService::new(Box::new(repo));
assert!(matches!(svc.get_user("abc"), Err(Error::NotFound)));
}
Async Tests (Concurrency P10 Test for Race Conditions, Testing P1 Tests as Specification)
#[tokio::test]forasync fntests: runs the coroutine on a tokio runtime. Without it, anasync fntest returns a future, never awaited (silently passes).#[tokio::test(flavor = "multi_thread")]for concurrency-sensitive tests: multi-thread runtime surfaces races that single-thread misses (Concurrency P10).tokio::time::pause()andadvance()for time: freeze and advance the runtime clock deterministically. Notokio::time::sleep(real)in tests.- Race-sensitive tests use
loomfor model-checking:loomsimulates all thread interleavings; it catches races-race-style detectors miss. Use for lock-free data structures.
#[tokio::test]
async fn async_fetch_returns_user() {
let u = fetch_user("abc").await.unwrap();
assert!(!u.name.is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_cache_is_safe() {
let c = Arc::new(Cache::new());
let mut h = vec![];
for i in 0..10 {
let c = c.clone();
h.push(tokio::spawn(async move { c.get(&i.to_string()).await; }));
}
for x in h { x.await.unwrap(); }
}
Determinism and Time (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- No
SystemTime::now()orInstant::now()in code under test: inject aClocktrait. In tests, a fake clock advances deterministically. tokio::time::pause()for async time: freezes the runtime clock;tokio::time::advance(dur)moves it. Asleep(5s)in test resolves instantly.--test-threads=1to reproduce order coupling: by default,cargo testruns tests in parallel; a test that passes alone but fails in a suite has hidden shared state.-1reproduces.
trait Clock { fn now(&self) -> std::time::Instant; }
struct FakeClock(std::time::Instant);
impl Clock for FakeClock {
fn now(&self) -> std::time::Instant { self.0 }
}
#[test]
fn user_has_created_at() {
let clk = FakeClock(std::time::Instant::now());
let u = create_user_with_clock("a@b.co", &clk).unwrap();
assert_eq!(u.created_at, clk.now());
}
Doc Tests (Documentation P1 Documentation is Code, Testing P1 Tests as Specification)
cargo test --docruns///fenced blocks: a///example with#-hidden setup is a tested artifact; a stale output fails the build (Documentation P1).no_runfor examples that should compile but not run:```rust,no_run— type-checks the example without executing. Use for examples that need a DB.ignorefor examples that should not compile-check:```rust,ignore— skips entirely. Rare; preferno_run.- Applies
Testing P1: the doc example is the spec; the doc test is the spec's regression test.
/// Fetch a user by id.
///
/// # Example
///
/// ```
/// # use mycrate::{get_user, Error};
/// let u = get_user("abc").unwrap();
/// assert!(!u.name.is_empty());
/// ```
pub fn get_user(id: &str) -> Result<User, Error> { /* ... */ }
Cross-References
domains/testing/pyramid.md— where unit/property/doc tests sit; proptest is the property layer.domains/testing/fixtures.md—t.Cleanup-equivalent (Dropin tests) as fixture discipline.domains/testing/first-principles.md— Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage.domains/concurrency/first-principles.md— Concurrency P10 (test for races),loommodel-checking.languages/rs-ownership.md—Send/Synctests and ownership-based property tests.languages/rs-async.md—#[tokio::test]patterns from that doc.languages/rs-tooling.md—cargo testflags (--doc,--test-threads) detailed here.