Rust Testing Strategy

SkillDev tools

HASH Rust testing strategy. Use when writing Rust unit, integration, or snapshot tests, or choosing assertion and test-organization patterns.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Rust Testing Strategy skill

What this skill tells your AI

The instructions your AI receives, as published by hashintel/hash in .agents/skills/rust-testing-strategy/SKILL.md and read by ahel’s review.

Verification Commands

  • When modifying Rust code, run clippy to detect issues:

    cargo clippy --all-features --all-targets --workspace --no-deps
    
  • Use cargo doc --no-deps --all-features to check documentation

Test Execution

  • Use cargo-nextest for running unit and integration tests
  • Use the default test runner for documentation tests
  • For comprehensive verification, run the complete test suite
  • Database seeding can be performed using yarn commands in the project's package.json

Test Design Principles

  • Test both happy paths and error conditions for each function
  • For each error case in your code, write a corresponding test
  • Test boundary conditions and edge cases explicitly
  • Include tests for invalid or malformed inputs
  • For streaming encoders/decoders, test partial data handling and buffer management
  • Aim for high test coverage but prioritize test quality over quantity
  • Structure tests following the Arrange-Act-Assert pattern

Assertion Standards

  • Use descriptive assertion messages that explain the expected behavior

  • All assertion messages (including expect(), unwrap() and assert*()) should follow the "should..." format:

    // Good examples:
    value.expect("should contain a valid configuration");
    assert_eq!(result, expected, "Result should match the expected value");
    
  • Use expect() or expect_err() with clear messages instead of unwrap() or unwrap_err()

  • Prefer assert_eq! with custom messages over bare assertions when comparing values

  • When testing errors, assert on specific error types or message contents, not just that an error occurred

  • Balance assertions to verify functionality without creating brittle tests

Test Organization

  • Group related tests into appropriate modules
  • Use helper functions to avoid code duplication in tests
  • Consider using parameterized tests for testing similar functionality with different inputs

Test Naming

A test name is a short label with the shape <subject>_<case>[_<variant>]:

  • <subject> is the operation, type or function under test. It does not repeat the name of the enclosing module, because the test path already carries that.
  • <case> is what distinguishes this test from its siblings under the same subject: an operand shape, an input class or a code path.
  • <variant> narrows the case further when two tests share one, and is otherwise absent.

The shape buys two things. Tests of one subject share a prefix, so a module's test list reads as a table of subject × case and cargo nextest run -E 'test(<subject>_)' selects the family. And the name says what the test covers while the doc comment says what it asserts and why, so neither repeats the other.

What a name never carries:

  • A test_ prefix, an article, a narration verb (should, works, correctly) or a when/with/that clause. The story a sentence-name would tell belongs in the test's doc comment and its assertion messages.
  • More than about six words. Three to five is the norm, and a longer name is a test doing too much, where the fix is one test per case.
  • The rejection, for a negative case. The name is the input class (ice_invalid_subscript_type, rank_positions_short, rows_out_of_domain), with no err_ prefix or _err suffix. An outcome word is the final token only when the case alone is ambiguous (eq_same_type_accepted).

Where the shape meets a test framework:

  • A property test, or an rstest case set, takes its name from the property it tests (lattice_laws, solve_anti_symmetry, condensation_is_dag).
  • Renaming a snapshot test renames the snapshot files derived from its name in the same change.
// Before: a sentence, three cases in one test, the outcome in the name.
async fn rank_pair_tampers_name_their_own_variants() { /* … */ }
async fn short_node_identity_table_refuses() { /* … */ }

// After: one label per case, the outcome in the doc comment.
/// Open refuses a reverse rank permutation short of the code column, under `Columns`.
async fn rank_positions_short() { /* … */ }
/// Open refuses a reverse rank permutation that is no permutation, under `RankInverse`.
async fn rank_positions_constant() { /* … */ }
/// Open refuses a node identity table short of the code column, under `Identities`.
async fn node_identities_short() { /* … */ }

Test Code Quality

  • Follow the same code quality standards in test code as in production code
  • Add appropriate assertions for array/slice access to avoid clippy warnings
  • Document test scenarios with clear comments explaining:
    • The setup (input and environment)
    • The action being tested
    • The expected outcome
    • Why the outcome is expected
  • Consider adding custom test utilities to simplify common testing patterns
  • Use the json! macro from serde_json instead of constructing JSON as raw strings
// Bad:
let json_str = "{\"name\":\"value\",\"nested\":{\"key\":42}}";

// Good:
use serde_json::json;
let json_value = json!({
    "name": "value",
    "nested": {
        "key": 42
    }
});

Signals

GitHub stars
2k
Forks
123
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
rust-testing-strategy
Source
github.com/hashintel/hash