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.

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.