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()inapp.rs— returns anAppwith an in-memory DB and connected state.db()indb.rs— returns an in-memoryDatabase.
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
| Situation | Approach |
|---|---|
Test needs an App or Database | Use the fixture (#[rstest] + parameter) |
| 3+ tests with identical structure, different data | Parameterize with #[case] |
| 2 tests with significantly different setup | Keep them separate |
| Test doesn’t need a fixture | Plain #[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 andmatch. - Add a
_labelparameter 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
matchwith 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
| Target | What it tests |
|---|---|
fuzz_json_rpc | JSON-RPC deserialization and parse_signal_event / parse_rpc_result |
fuzz_input_edit | UTF-8 cursor navigation and string mutation at byte boundaries |
fuzz_key_combo | parse_key_combo with arbitrary strings from user TOML files |
fuzz_command_parse | parse_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.