--- title: "Nim Testing: unittest, nimble test, and testament (2026)" description: "Verified guidance on Nim's std/unittest module (check vs require, suites, setup/teardown), nimble test conventions, when testament's compiler-oriented test runner is and is not appropriate for application code, and the honest current state of property-based testing in Nim." language: nim framework: null category: language_testing tags: - testing - unittest - testament - nimble - unit-testing - integration-testing - property-based-testing - tdd keywords: - nim std/unittest check vs require - nimble test tests directory convention - testament nim compiler test runner - nim property based testing library status - nim test suite compile time nimcache last_updated: "2026-07-07" difficulty: intermediate version: "Nim >= 2.0 (stable line 2.2, currently 2.2.10, April 2026)" related: - README.md - best_practices.md - common_pitfalls.md - metaprogramming.md - concurrency.md - interop_ffi.md - ../../core/testing/unit_testing_principles.md - ../../core/devops/autonomous_least_privilege_environments.md search_priority: high status: published --- # Nim Testing: unittest, nimble test, and testament (2026) ## Introduction / Overview Nim's testing landscape has three layers that serve different purposes and must not be confused: **`std/unittest`**, the stdlib assertion/grouping module that most projects write their tests against; **`nimble test`**, the package manager's convention-based command that discovers and runs those tests; and **`testament`**, the Nim compiler's own internal test runner, which is occasionally, but not typically, adopted by application projects. A fourth area - property-based testing - remains genuinely immature in the Nim ecosystem as of this writing, which is itself an important, verified fact for anyone (human or LLM) reaching for a Hypothesis- or proptest-equivalent library by habit. This document is verified against the current `std/unittest` documentation, Nimble's test-task documentation, and the `testament` documentation and its own stated purpose in the Nim compiler repository. Facts that are fast-moving or not fully settled in the ecosystem (property-based testing library maturity, incremental compilation status) are flagged explicitly rather than asserted as fact. **When to use this guidance**: Setting up a test suite for a new Nim library or application, deciding whether `testament` is worth adopting outside compiler development, or wiring `nimble test` into CI. ## Core Concepts - **`check` continues, `require` aborts**: within a `std/unittest` `test` block, a failed `check` records the failure and keeps executing the remaining statements in that test; a failed `require` **quits the entire test binary immediately**, skipping any remaining checks and any `teardown` block. This is a load-bearing distinction - using `require` where `check` was intended silently skips test coverage and teardown on the first failure of a run. - **`suite` groups tests and shares `setup`/`teardown`**: a `suite "name":` block groups related `test "...":` blocks and can declare `setup:` (run before each test in the suite) and `teardown:` (run after each test, but only if the test did not `require`-abort). - **`nimble test` is convention-based, not configuration-based by default**: with no `task test` defined in the `.nimble` file, Nimble compiles and runs every file in `tests/` whose name begins with `t` (e.g. `tests/tinventory.nim`); a project can override this entirely with a custom `task test, "...": exec "..."` block. - **`testament` is, first and foremost, the tool that tests the Nim compiler and standard library** - its own documentation describes it as responsible for executing the compiler's and stdlib's test suites, with process isolation, multi-backend (C/C++/JS) support, and rich expected-output specification per test file. It is a legitimate, if uncommon, choice for application projects that specifically need that process isolation and multi-backend matrix; it is not the default recommendation for ordinary application/library testing, where `std/unittest` remains the norm. - **Property-based testing is immature in Nim as of this writing**: the closest analogue to Python's Hypothesis or Rust's `proptest`, `nim-works/npbt`, is explicitly labeled **work-in-progress** by its own repository. There is no widely adopted, production-grade property-based testing library in the Nim ecosystem today - treat any code that assumes one exists with the same skepticism as an assumed-but-unverified stdlib function. - **Each test file is typically its own compilation unit**: `nim c -r tests/tfoo.nim` performs full semantic analysis, C code generation, and a C-compiler invocation for that one file. A test suite spread across many small files pays this overhead once per file; consolidating related tests into fewer files/binaries amortizes it. ## Best Practices ### 1. Structure suites with `suite`, `setup`, and `teardown` **Rationale**: Grouping related tests under a `suite` block with shared `setup`/`teardown` avoids duplicating fixture logic in every `test` block, mirroring the role fixtures play in `pytest` (see [Python Testing Guide](../python/testing.md#1-design-fixtures-with-the-narrowest-correct-scope)) without a dependency-injection mechanism of its own. ```nim import std/unittest type Inventory = object stock: int suite "inventory management": var inventory: Inventory setup: inventory = Inventory(stock: 10) teardown: inventory.stock = 0 test "reserving stock decreases the count": inventory.stock -= 3 check inventory.stock == 7 test "reserving more than available stock fails": proc reserve(inv: var Inventory, qty: int) = if qty > inv.stock: raise newException(ValueError, "insufficient stock") inv.stock -= qty expect ValueError: reserve(inventory, 999) ``` **Why this works well in production**: `setup` re-runs before every `test` in the suite, giving each test a fresh `inventory` value with no leakage between tests - the same isolation guarantee `function`-scoped `pytest` fixtures provide, without needing to reason about fixture scope explicitly, since `std/unittest` has only this one, always-per-test granularity. ### 2. Use `check` for assertions that should not halt the rest of the test, `require` only for preconditions **Rationale**: A test verifying multiple independent properties of one result should use `check` for each property so a single failing assertion does not hide failures in the others reported in the same run; `require` is reserved for a genuine precondition without which the rest of the test body is meaningless (e.g., a setup call that must succeed before anything else can be asserted). ```nim import std/unittest test "parsing a well-formed record": let record = parseRecord("42,active,2026-07-07") require record.isSome # nothing else makes sense to check if this fails check record.get.id == 42 check record.get.status == "active" check record.get.date == "2026-07-07" ``` **Production tip**: `require`'s abort behavior can be widened to make *every* `check` in the binary behave like `require` via `-d:nimUnittestAbortOnError:on` - useful transiently while debugging a single failing test locally (stop at the first failure instead of scrolling through a long report), but should not be a permanent CI setting, since it silently reduces the number of independent failures a single CI run reports. ### 3. Let `nimble test` discover tests by convention, and pin the source path with `tests/config.nims` **Rationale**: Nimble's default `test` task compiles and runs every `tests/t*.nim` file without any configuration; the one piece of setup nearly every project needs is telling those test files where to find the package's own source, since a test file living in `tests/` does not automatically see `src/`. ``` orders/ ├── orders.nimble ├── src/ │ └── orders/ │ └── inventory.nim └── tests/ ├── config.nims # switch("path", "$projectDir/../src") ├── tinventory.nim └── tbilling.nim ``` ```nim # tests/config.nims switch("path", "$projectDir/../src") ``` ```bash nimble test ``` **Why this works well in production**: this convention requires zero entries in the `.nimble` file itself - Nimble's built-in `test` task already knows to look in `tests/`, and `config.nims` is picked up automatically by any `nim c`/`nim r` invocation rooted in that directory, including the one `nimble test` performs internally. ### 4. Define an explicit `task test` when the default convention is not enough **Rationale**: Projects that need a single consolidated test binary (to amortize compile time - see Core Concepts), a non-default test directory, or extra compiler flags (e.g., `--mm:arc` for a test matrix against multiple memory-management modes) should override Nimble's default task explicitly rather than fight it. ```nim # orders.nimble task test, "Run the full test suite": exec "nim c -r --mm:orc tests/tester.nim" exec "nim c -r --mm:arc tests/tester.nim" ``` ```nim # tests/tester.nim -- a single consolidated binary importing every test module import tinventory import tbilling ``` **Why this works well in production**: compiling once and running a consolidated `tester.nim` (which simply imports every individual test module so their top-level `test`/`suite` blocks register and run) avoids paying full compile-and-link overhead once per test file, which matters once a suite grows past a few dozen files. ### 5. Reach for `testament` only when you specifically need process isolation or a multi-backend matrix **Rationale**: `testament` ships with the Nim compiler and is what the Nim project itself uses to validate the compiler and standard library - process-isolated execution per test, `.nim` test files annotated with an expected-output/exit-code specification block, support for running the same test against multiple backends (C, C++, JS) in one pass, categorized test suites, and HTML report generation. Its specification format embeds expectations directly in the test file: ```nim discard """ exitcode: 0 output: ''' 7 ''' """ echo 3 + 4 ``` ```bash testament pattern "tests/*.nim" testament category mycategory ``` **When this is worth adopting for an application**: a project that must verify identical behavior across the C, C++, and JS backends (a library specifically designed to be portable across all three), or one that needs strict process isolation per test case (fuzzing-adjacent test suites, tests that may crash the process and must not take down the rest of the run) benefits from `testament`'s design. Absent one of those specific needs, the added specification-file format and megatest/batching machinery are unnecessary ceremony compared to `std/unittest` plus `nimble test`. ### 6. Fake dependencies through explicit interfaces rather than reaching for a mocking framework **Rationale**: Nim has no dominant, widely-adopted mocking framework comparable to Python's `unittest.mock`/`pytest-mock` or Rust's `mockall`. The idiomatic pattern - closer to Go's or Rust's trait-based faking than to Python's monkey-patch-based mocking - is to depend on a small `object`/procedural interface (a set of `proc` types stored in an object, or a `concept`/generic parameter) and substitute a fake implementation in tests. ```nim type PaymentGateway = object charge: proc (amountCents: int): bool {.closure.} proc newStripeGateway(): PaymentGateway = PaymentGateway(charge: proc (amountCents: int): bool = # real HTTP call to Stripe true ) proc newFakeGateway(shouldSucceed: bool): PaymentGateway = PaymentGateway(charge: proc (amountCents: int): bool = shouldSucceed) proc checkout(gateway: PaymentGateway, amountCents: int): bool = gateway.charge(amountCents) test "checkout fails cleanly when the gateway declines": let gateway = newFakeGateway(shouldSucceed = false) check checkout(gateway, 500) == false ``` **Why this works well in production**: this pattern requires no additional dependency, is fully type-checked at compile time (a fake with the wrong signature fails to compile rather than failing at test run time), and reads as ordinary Nim rather than framework-specific mocking DSL syntax. ## Common Pitfalls & Anti-Patterns ### Pitfall: Adopting `testament` by default for ordinary application testing **Problem**: `testament` is documented and primarily used as the Nim compiler's own internal test-suite runner. An LLM that sees `testament` referenced in Nim compiler documentation may over-index on it and generate a `testament`-based test suite for an ordinary web service or CLI tool, adding a specification-file format and a runner with a much larger surface area than the problem requires. **Recommended approach**: Default to `std/unittest` plus `nimble test` for application and library code; reach for `testament` only per the specific criteria in Best Practice #5 (multi-backend verification, process-isolated crash-tolerant test runs). ### Pitfall: Using `require` where `check` was intended **Problem**: A `test` block with several `require` calls in a row silently stops reporting after the first failure - subsequent assertions in the same test never execute, and CI output shows only "1 failure" when several properties may actually be broken, misleading whoever triages the failure. **Recommended approach**: Default to `check` for every assertion that is not itself a hard precondition for the rest of the test; reserve `require` for setup calls whose failure makes the remaining assertions meaningless (see Best Practice #2). ### Pitfall: Assuming a mature property-based testing library exists **Problem**: Generated Nim test code that imports a nonexistent or barely-started property-based testing package (by analogy by generalizing habits from Python/Rust) will fail to compile, or will pull in `nim-works/npbt` and hit its documented work-in-progress limitations. **Recommended approach**: Default to manual, example-based `check`/`require` assertions with deliberately chosen boundary values (empty sequences, zero, negative numbers, Unicode edge cases) written by hand; if property-based testing is genuinely required, verify `npbt`'s current state directly against its repository before depending on it, and budget for filling gaps yourself. ### Pitfall: One `.nim` file per test case in a large suite **Problem**: Each additional test file is a separate `nim c` invocation - separate semantic analysis, C codegen, and C-compiler/linker invocation - so a suite with hundreds of one-test files pays large, repeated fixed overhead independent of the tests' own runtime. **Recommended approach**: Group related tests into fewer files per logical suite (see Best Practice #4's consolidated `tester.nim` pattern), and rely on `nimcache` object-file reuse (the default incremental C-object caching Nim already performs between compiles) rather than fragmenting further. ## Testing Strategies - **Unit tests**: pure functions and business logic, expressed as `test` blocks inside a `suite`, asserted with `check`; the majority of a well-structured suite. - **Integration tests**: real database/file-system/network interaction, isolated into their own suite or files so they can be excluded from a fast default `nimble test` run when needed (e.g., via a build-time flag checked with `when defined(integrationTests):`). - **Exception-path tests**: `expect SomeException: ...` (a `std/unittest` template) asserts that a block raises a specific exception type; prefer it over manually wrapping a `try`/`except` and asserting a boolean flag was set. - **Concurrency tests**: see [Nim Concurrency](concurrency.md#testing-strategies) for thread/async-specific testing guidance - these should not be mixed into ordinary `std/unittest` suites without accounting for timing nondeterminism. - **Recommended test stack for a typical Nim project in 2026**: `std/unittest` for assertions and grouping, `nimble test` (default convention, or a custom `task test`) as the CI entry point, hand-written boundary-value cases in place of property-based testing until the ecosystem's tooling matures further. ## Performance, Security & Scalability Considerations - **Compile time is part of your test suite's cost**: unlike interpreted-language test runners, every `nim c -r` pays full compilation before a single assertion runs; consolidating test files (Best Practice #4) and relying on `nimcache`'s object-file reuse between runs are the practical levers available today. - **Whole-program incremental compilation (IC) is not yet a reliable, general-purpose speedup**: Nim has an ongoing, multi-year effort toward incremental compilation (tied to newer compiler-frontend work), but as of this writing it is not a settled, universally-enabled feature of stable releases - do not assume `--incremental` or similar flags will reliably cut test-suite compile time in a current stable Nim installation; verify against the specific installed version's release notes. - **CI non-interactive test execution**: `nimble test` and `choosenim`-installed toolchains run entirely under the invoking user's own account with no elevated privileges required, which matters for CI runners and autonomous agent sandboxes (see [Autonomous, Least-Privilege, Portable Execution Environments](../../core/devops/autonomous_least_privilege_environments.md)) - never invoke `nimble`/`choosenim` via `sudo` in CI, since both are designed to install and operate entirely within the user's home directory. - **Test isolation across processes**: `std/unittest`-based suites run all tests in one process by default; a test that corrupts global state (a mutated module-level `var`) can silently affect a later test in the same binary - prefer function-local state over module-level `var`s in test helpers for exactly this reason. ## Edge Cases & Advanced Usage - **Parametrized-style tests without a built-in `parametrize`**: `std/unittest` has no direct equivalent to `pytest.mark.parametrize`; the idiomatic Nim pattern is a `for` loop over a `seq` of input/expected-output tuples inside a single `test` block, with `check` (not `require`) per case so all cases are evaluated and reported even if one fails. ```nim test "normalize trims and lowercases": let cases = @[ (" Hello ", "hello"), ("WORLD", "world"), ("", ""), ] for (input, expected) in cases: check normalize(input) == expected ``` - **Testing exception-effect (`{.raises.}`) correctness**: because Nim's effect system is checked at compile time, a proc's declared `{.raises: [...].}` set is itself part of its public contract; a `nim check` invocation (not just a runtime test) that fails to compile because a proc's body can raise something outside its declared set is a legitimate part of a project's test/CI matrix, distinct from `std/unittest` runtime assertions. - **`unittest2`** (`status-im/nim-unittest2`): a drop-in-compatible evolution of `std/unittest` from the Status organization, offering performance and API refinements while preserving the `test`/`suite`/`check`/`require` vocabulary; verify its current feature delta against stdlib `unittest` before adopting if compatibility with vanilla `std/unittest`-based tooling matters. ## When *Not* to Use / Alternatives | Dimension | `std/unittest` + `nimble test` | `testament` | |---|---|---| | Setup complexity | Minimal - write `test`/`suite` blocks, run `nimble test` | Higher - expected-output specification blocks, category/pattern invocation | | Multi-backend (C/C++/JS) verification | Manual (separate `nim c`/`nim cpp`/`nim js` invocations per suite) | Built in | | Process isolation per test | No (one process runs the whole suite by default) | Yes | | Ecosystem familiarity | High - the default assumption in nearly all Nim tutorials and libraries | Lower - primarily associated with Nim compiler development | | Best fit | Ordinary application/library test suites | Compiler/language-tooling projects, or libraries needing verified cross-backend behavior | **Choose `testament`** only when process isolation or a multi-backend test matrix is a genuine, stated requirement - not by default because it ships with the compiler. **Choose `unittest2`** over stdlib `unittest` only after confirming its current feature set still tracks closely enough with stdlib `unittest` for your team's familiarity needs; otherwise stdlib `unittest` remains the safer default for a codebase other contributors will recognize immediately. ## Related Documentation - [Nim Language Overview & Ecosystem](README.md) - [Nim Best Practices](best_practices.md) - [Nim Common Pitfalls & Anti-Patterns](common_pitfalls.md) - [Nim Metaprogramming](metaprogramming.md) - [Nim Concurrency](concurrency.md) - [Nim FFI & Interop](interop_ffi.md) - [Unit Testing Principles](../../core/testing/unit_testing_principles.md) - [Autonomous, Least-Privilege, Portable Execution Environments](../../core/devops/autonomous_least_privilege_environments.md) ## Glossary - **`check`**: A `std/unittest` assertion that records a failure but allows the remaining statements in the current `test` block to continue executing. - **`require`**: A `std/unittest` assertion that, on failure, immediately quits the entire test binary, skipping remaining checks and `teardown`. - **`suite`**: A `std/unittest` construct grouping related `test` blocks, with optional shared `setup`/`teardown`. - **`testament`**: Nim's own compiler/stdlib test-suite runner, with process isolation, multi-backend support, and an expected-output specification format embedded in each test file. - **`nimcache`**: The directory Nim uses to cache generated C/C++ files and object files between compiles, providing partial, object-level build speedup independent of whole-program incremental compilation. - **Property-based testing**: A testing style generating inputs from a strategy rather than hand-picking examples; **not yet mature in the Nim ecosystem** as of this writing (`nim-works/npbt` is work-in-progress). --- **Maintenance note**: When updating this document, re-verify `npbt`'s maturity and any incremental-compilation (IC) progress against the `nim-lang/Nim` repository, since both are explicitly flagged here as fast-moving and unsettled.