Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Running tests

Run the full test suite:

cargo test

Run tests for a specific module:

cargo test app::tests          # App module tests
cargo test signal::client::tests  # Signal client tests
cargo test db::tests           # Database tests
cargo test input::tests        # Input parsing tests

Run a single test by name:

cargo test test_name

rstest

Tests use rstest for fixtures and parameterization. The crate is declared in [dev-dependencies].

Fixtures

Two #[fixture] functions provide pre-built test objects:

  • app() in app.rs — returns an App with an in-memory DB and connected state.
  • db() in db.rs — returns an in-memory Database.

To use a fixture, mark the test #[rstest] and add the fixture as a parameter:

#![allow(unused)]
fn main() {
#[rstest]
fn my_test(mut app: App) {
    app.input.buffer = "/quit".to_string();
    // ...
}
}

Parameterized tests

When multiple tests share the same assertion logic but differ in inputs, use #[case] to collapse them into a single function:

#![allow(unused)]
fn main() {
#[rstest]
#[case("/quit", InputAction::Quit)]
#[case("/q",    InputAction::Quit)]
#[case("/help", InputAction::Help)]
fn command_returns_expected_action(#[case] input: &str, #[case] expected: InputAction) {
    assert_eq!(parse_input(input), expected);
}
}

Each #[case] produces a separate entry in cargo test output, so individual failures are easy to identify.

When to use what

SituationApproach
Test needs an App or DatabaseUse the fixture (#[rstest] + parameter)
3+ tests with identical structure, different dataParameterize with #[case]
2 tests with significantly different setupKeep them separate
Test doesn’t need a fixturePlain #[test] is fine

Best practices

  • Prefer #[case] over copy-paste. If you’re writing a new test and an existing parameterized test covers the same assertion pattern, add a #[case] instead of a new function.
  • Keep case data simple. Strings, numbers, and booleans work well in #[case] attributes. For complex types, build them inside the test body using a label parameter and match.
  • Add a _label parameter when case data alone doesn’t make the purpose obvious. This shows up in test names (e.g., my_test::case_basic).
  • Don’t over-parameterize. If merging tests requires a match with completely different setup logic per arm, separate tests are clearer.

Snapshot tests (insta)

Integration snapshot tests use insta with ratatui’s TestBackend to render the full UI and compare against committed .snap files. This catches layout regressions, missing overlays, and rendering bugs automatically.

Running snapshot tests

cargo test snapshot_tests

Accepting new snapshots

When you change the UI, snapshot tests will fail with a diff. Use cargo-insta to review and accept:

cargo install cargo-insta  # first time only
cargo insta accept

Accepted snapshots are committed as .snap files in src/snapshots/.

Test helpers

The snapshot test module (ui::snapshot_tests) provides:

  • demo_app() – creates an App with in-memory DB, connected state, and deterministic demo data (fixed date for stable timestamps)
  • render_to_string(app, width, height) – renders via TestBackend and returns the buffer as a trimmed string

Coverage

Snapshot tests cover:

  • Sidebar layout and conversation list
  • Chat messages (quotes, link previews, edited messages, reactions)
  • Normal vs Insert mode indicator
  • Help, settings, and about overlays
  • Narrow terminal (sidebar auto-hide)
  • Styled text (bold, monospace)
  • Polls, pinned messages, unread markers
  • Empty conversations, message requests, disappearing messages
  • Sidebar filter

Test modules

Tests are defined as #[cfg(test)] mod tests blocks within each source file.

db.rs tests

Database tests use Database::open_in_memory() for isolated, fast test instances. Coverage includes:

  • Schema migration and table creation
  • Conversation upsert and loading
  • Name updates on conflict
  • Message insertion and retrieval (ordering)
  • Unread count with read markers
  • System message exclusion from unread counts
  • Conversation ordering by most recent message
  • Mute flag round-trip
  • Last message rowid tracking

input.rs tests

Input parser tests cover:

  • Plain text passthrough
  • Empty and whitespace-only input
  • All commands and their aliases (/join, /j, /part, /p, etc.)
  • Commands with and without arguments
  • Unknown command handling

app.rs tests

Application state tests cover signal event handling, conversation management, and mode transitions.

signal/client.rs tests

Signal client tests cover JSON-RPC parsing and event routing.

Demo mode for manual testing

cargo run -- --demo

Demo mode populates the UI with dummy conversations and messages without requiring signal-cli. This is the easiest way to manually test UI changes, keybindings, and rendering.

Linting

The project enforces zero clippy warnings:

cargo clippy --tests -- -D warnings

CI runs this on every push and pull request. Fix all warnings before pushing.

Fuzz testing

The fuzz/ directory contains cargo-fuzz harnesses for external input boundaries. Fuzz testing requires nightly Rust and Linux or macOS (libfuzzer does not support Windows).

cargo install cargo-fuzz
cargo +nightly fuzz run <target>

Fuzz targets

TargetWhat it tests
fuzz_json_rpcJSON-RPC deserialization and parse_signal_event / parse_rpc_result
fuzz_input_editUTF-8 cursor navigation and string mutation at byte boundaries
fuzz_key_comboparse_key_combo with arbitrary strings from user TOML files
fuzz_command_parseparse_input with arbitrary slash commands

Run cargo fuzz list to see all available targets. Any panic found by the fuzzer is a bug to fix.