Quickstart
unittest2 is a Nim module for writing automated tests.
It is mostly compatible with Nim's built-in unittest, while adding better output, flexible reporting, and advanced execution features like test discovery.
Installation
$ nimble install -y unittest2
Add unittest2 to your .nimble file:
requires "unittest2"
First Test File
unittest2 follows this test structure:
- Module. A .nim file that has an
import unittest2in it, usually sits intestsdirectory of your project, and is named starting with atby convention, e.g.tmath.nim. A module holds the tests that cover a particular topic or a module in your project. - Suite. Defined with
suiteblock, it's a group of related tests. In hetmath.nimexample, you could have a suite like "Overflow checks" and "Basic arithmetic." - Test case. A single atomic check under a
testblock. Intmath.nimsuite "Basic arithmetic," you could have tests like "Addition"and "Subtraction."
Aside from test, you can use setup and teardown blocks inside suite.
setup block defines the code that runs before each test.
teardown block defines the code that runs after each test.
Create test.nim:
import unittest2
# A suite groups related tests and can share common setup/teardown code.
suite "math":
setup:
# Code inside setup runs before every single test in this suite.
discard
test "2 + 2 == 4":
# 'check' verifies that a condition is true.
# If it fails, the test is marked as FAILED and the values are printed.
check:
1 + 1 == 2
test "out of bounds raises":
let xs = @[1, 2, 3]
# 'expect' verifies that the code block raises a specific exception.
# It is useful for testing error conditions.
expect IndexDefect:
discard xs[4]
Compile and run:
nim c -r test.nim
This exits with 0 on success and 1 when any test fails.
See command options:
# Running the compiled binary directly
./test --help
Compilation Flags
unittest2 behavior can be customized using compilation flags (-d:flag).
A detailed list of all available flags, including execution modes and advanced features, can be found on the Compilation Flags page.
Compile-Time Testing
One of unittest2 standout features is the ability to run tests during compilation. Use staticTest, runtimeTest, or dualTest for explicit control over when your tests execute.
You can also opt into compile-time execution globally for all test blocks:
nim c -d:unittest2Static=true test.nim
Contributing and Testing This Repository
Run tests locally:
# this calls a task in "config.nims"
nim test
Build the documentation locally:
# Build both the book and apidocs
nim docs
# Build only the book
nim book
# Build only the apidocs
nim apidocs
Navigate the generated docs:
Tip: start from the API index, then jump to symbols in unittest2.nim for implementation details.
Running and Filtering Tests
When your test suite grows, running every single test every time can become slow and noisy. unittest2 allows you to pick exactly which tests you want to execute. You can run a single test case, all tests in a specific suite, or a group of tests that match a certain naming pattern.
To filter tests, you pass filter arguments to the compiled test binary. These arguments come after any regular options (like --xml or --verbose).
A filter can target:
- An exact test name.
- A specific test inside a specific suite using the
suite::testsyntax. - An entire suite using the
suite::syntax. - Multiple tests or suites using a
*wildcard.
If you provide multiple filter arguments, unittest2 will run any test that matches at least one of them.
Run Individual Tests
Using the tests from the Quickstart example:
# Runs only the "2 + 2 == 4" test
nim c -r test.nim "2 + 2 == 4"
# Runs both specified tests
nim c -r test.nim "2 + 2 == 4" "out of bounds raises"
If a test name is unique across your entire project, you can simply use its name as the filter.
Run a Single Suite
To run all tests within the math suite, use the suite name followed by two colons (::):
nim c -r test.nim "math::"
Run a Specific Test in One Specific Suite
If multiple suites have tests with the same name, or if you want to be explicit, use the suite::test syntax:
nim c -r test.nim "math::2 + 2 == 4"
Wildcard Pattern Matching
Filters support a wildcard * to match partial names.
You can use exactly one asterisk per filter. Patterns like *bug* are not supported and will look for a literal * at the end.
The asterisk can be placed at the beginning, end, or in the middle of a filter:
# Run all suites that start with "ma"
nim c -r test.nim "ma*::"
# Run tests in the "math" suite that start with "out"
nim c -r test.nim "math::out*"
# Run all tests that end with "raises"
nim c -r test.nim "*raises"
# Match "2 + 2 == 4" using a wildcard
nim c -r test.nim "2 * 4"
Notes on Matching
testOnlymatches any test named exactlytestOnly, regardless of which suite it belongs to.- If a test name itself contains
::,unittest2will still correctly match it as an exact test-name filter.
Example with :: inside the test name:
nim c -r test.nim "HTTP :: returns 200"
Command-Line Options
unittest2 supports runtime options via command-line arguments and environment variables. Command-line options take precedence.
Core Flags
--help: Print usage and exit.--xml:<file>or--xml=<file>: Write a JUnit-compatible XML report usingJUnitOutputFormatter.--console: Force console output formatter (ConsoleOutputFormatter).--output-level:<level>or--output-level=<level>: Set theOutputLevel. One ofVERBOSE,COMPACT,FAILURES,NONE.--verboseor-v: Shortcut for--output-level=VERBOSE.
Environment Variables
You can configure unittest2 using environment variables. To maintain compatibility with the built-in unittest module, both UNITTEST2_ and NIMTEST_ prefixes are supported.
| Variable | Values | Description |
|---|---|---|
UNITTEST2_OUTPUT_LVL | VERBOSE, COMPACT, FAILURES, NONE | Sets the default output verbosity. |
UNITTEST2_ABORT_ON_ERROR | (any) | If defined, stops the test run immediately after the first failure. |
NIMTEST_COLOR | always, never | Explicitly enables or disables colored output. |
NIMTEST_NO_COLOR | (any) | If defined, disables colored output (unless NIMTEST_COLOR is set to always). |
Compatibility Fallbacks
For seamless integration with existing tools and CI pipelines, unittest2 also recognizes these standard Nim testing variables:
NIMTEST_OUTPUT_LVL: Fallback forUNITTEST2_OUTPUT_LVL. RecognizesPRINT_ALL(VERBOSE),PRINT_FAILURES(FAILURES), andPRINT_NONE(NONE).NIMTEST_ABORT_ON_ERROR: Fallback forUNITTEST2_ABORT_ON_ERROR.
Parameter Parsing Behavior
By default, unittest2 parses the current process arguments automatically.
To disable this behavior (e.g., if your application needs to handle its own arguments):
nim c -d:unittest2DisableParamFiltering=true -r test.nim -- --your-app-args
Then you can call parseParameters manually in your test harness if you still want unittest2 to process some of the arguments.
Compilation Flags
unittest2 behavior can be customized using compilation flags (-d:flag). These flags allow you to select the execution model, enable advanced features, and configure compatibility with the standard library.
Execution Modes
-d:unittest2Compat=true|false: Select execution mode (default istrue).true: Compatibility Mode. Single-pass execution, identical tostd/unittest.false: Collect-and-Run Mode. Two-phase execution (discovery then run). See Operation Modes for details.- Set with
-d:unittest2Compat=falseto disable compatibility mode.
Advanced Features
-d:unittest2Static=true: Enable Compile-Time Execution.- Runs your tests in the Nim VM during compilation.
- Verifies that your code works correctly at compile-time.
- Use
staticTest,runtimeTest, ordualTestfor explicit control. - Set with
-d:unittest2Static=true.
-d:unittest2ListTests=true: List Tests without running them.- Useful for integration with external test runners or IDEs.
- Requires Collect-and-Run Mode (
-d:unittest2Compat=false). - Set with
-d:unittest2ListTests=true.
-d:unittest2DisableParamFiltering=true: Disable automatic argument parsing.- Use this if your test binary needs to handle its own CLI arguments. Call
parseParametersmanually if needed. - Set with
-d:unittest2DisableParamFiltering=true.
- Use this if your test binary needs to handle its own CLI arguments. Call
-d:unittest2NoCollect=true: Disable test collection mode.- This is an experimental feature that affects the order in which tests and suites are evaluated.
- Set with
-d:unittest2NoCollect=true.
-d:unittest2PreviewIsolate=true: Enable preview isolation mode.- Each test is run in a separate process to ensure complete isolation. This may be removed in the future.
- Set with
-d:unittest2PreviewIsolate=true.
Standard Library Compatibility
These flags provide compatibility with the original unittest module:
-d:nimUnittestOutputLevel=VERBOSE|COMPACT|FAILURES|NONE: Set the default output level at compile time.-d:nimUnittestColor=auto|on|off: Set the color output mode at compile time.-d:nimUnittestAbortOnError=true: Set whether to abort on the first error at compile time.
Operation Modes
unittest2 supports two distinct execution models: Compatibility Mode and Collect-and-Run Mode.
Compatibility Mode (Single Pass)
This is the default behavior (-d:unittest2Compat=true).
In this mode, unittest2 behaves like the standard unittest module: each test is executed immediately when its test block is reached. There is no separate discovery phase.
- Pros: Full compatibility with existing
unittestcode. - Cons: Limited reporting (cannot know total test count upfront), no support for advanced features like test listing or process isolation.
Collect-and-Run Mode (Two Phase)
Enable with:
nim c -d:unittest2Compat=false -r test.nim
In this mode, unittest2 execution is split into two phases:
- Discovery: The program runs, but
testblocks are only registered, not executed. - Execution: Once discovery is complete,
unittest2iterates through all registered suites and tests, executing them and reporting results.
Benefits
- Accurate Progress: Since the total number of tests that match your filters is known after discovery, formatters can show accurate
[1/50]progress indicators. - Test Listing: You can use
-d:unittest2ListTests=trueto see exactly which tests match your filters without executing them. - Advanced Filtering: This mode provides the foundation for advanced features like running filtered tests in separate processes or in parallel.
Preparing Your Test Suite
To ensure your test suite works correctly in Collect-and-Run mode, follow these guidelines:
- Avoid side effects in
suiteblocks: Any code inside asuiteblock but outside atest,setup, orteardownblock will execute during the Discovery phase. If this code modifies global state, it might affect tests in unexpected ways. - Use
setupandteardown: Always put initialization and cleanup logic insidesetupandteardown. These are guaranteed to run before and after each test during the Execution phase. - Independence: Ensure each test is independent. Collect-and-Run mode makes it easier to run tests in isolation, so they shouldn't rely on being run in a specific order.
Migration Caveats
- Top-level code: Code at the top level of your test module executes during Discovery.
- Global state: If you initialize global variables at the top level, they are initialized once before any test runs. If tests modify them, ensure they are reset in
setup.
Porting from std/unittest
Most test code can be ported from std/unittest with minimal changes, but there are important semantic differences when using certain execution modes.
Quick Porting Checklist
- Replace
import unittestwithimport unittest2. - Keep your existing
suite,test,check, andexpectblocks. - Reduce dependencies on global initialization order.
Execution Order and Discovery
The most significant difference occurs when using Collect-and-Run Mode (-d:unittest2Compat=false). Unlike std/unittest, which executes tests as they are encountered, unittest2 first "discovers" all tests before running any of them.
Code placed directly inside a suite block (but outside of test or setup) will execute during this discovery phase.
import unittest2
# In Collect-and-Run mode (-d:unittest2Compat=false),
# execution is split into Discovery and Execution phases.
suite "execution order":
# This code runs during the Discovery phase.
echo "1. Discovery phase: suite body runs"
setup:
# This runs during the Execution phase, before each test.
echo "3. Execution phase: setup runs before each test"
test "example test":
# This runs during the Execution phase.
echo "4. Execution phase: test body runs"
# This also runs during the Discovery phase.
echo "2. Discovery phase: suite body continues"
To maintain consistent behavior between modes, always place initialization logic inside setup and cleanup logic inside teardown.
Strategy for Large Codebases
- Migrate incrementally: Port one package or module at a time.
- Use Compatibility Mode initially: Keep
-d:unittest2Compat=true(the default) to match the single-pass execution ofstd/unittest. - Switch to Collect-and-Run Mode: Once your tests are independent and free of side effects in the
suitebody, disable compatibility mode to benefit from better reporting and advanced filtering.
Examples
Resource Lifecycle with setup and teardown
Use setup and teardown to manage resources that should be fresh for every test. This ensures that tests are independent and don't leak state to each other.
import unittest2
type FakeDb = object
connected: bool
writes: int
var db: FakeDb
suite "database repository":
setup:
db.connected = true
db.writes = 0
teardown:
db.connected = false
test "write increments counter":
check db.connected
db.writes.inc
check db.writes == 1
test "state is reset":
# db.writes will be 0 here because setup ran again
check db.writes == 0
Compile-Time Testing
One of unittest2 features is the ability to run tests during compilation using staticTest. This allows you to verify code logic in the Nim VM before the program even runs.
import unittest2
func fastFib(n: int): int =
if n <= 1:
n
else:
fastFib(n - 1) + fastFib(n - 2)
# This test runs while the compiler is building your project!
staticTest "fibonacci constant folding":
check fastFib(10) == 55
Data-Driven Tests
Data-driven tests cover many edge cases by iterating over a collection of inputs and expected outputs. This pattern keeps your test code compact and easy to extend.
import unittest2, std/strutils
proc parsePort(s: string): int =
let p = parseInt(s)
if p < 1 or p > 65535:
raise newException(ValueError, "port out of range")
p
suite "parsePort":
test "valid values":
# A collection of inputs and expected outputs
let cases = [("1", 1), ("80", 80), ("443", 443), ("65535", 65535)]
for (input, expected) in cases:
check parsePort(input) == expected
test "invalid values raise":
# A collection of inputs that should cause a ValueError
let inputs = ["0", "70000", "-1", "abc"]
for input in inputs:
expect ValueError:
discard parsePort(input)
Collect-and-Run Mode Optimization
When using the Collect-and-Run mode, it is important to separate the Discovery phase from the Execution phase. This example shows how to structure your suites for maximum compatibility.
import unittest2
# This file demonstrates patterns optimized for Collect-and-Run mode
# (-d:unittest2Compat=false)
suite "collect-and-run optimized suite":
# 1. Declarations inside the suite are safe, but avoid complex
# logic or side effects here, as they run during the Discovery phase.
var counter: int
setup:
# 2. Always initialize or reset state in setup.
# This runs during the Execution phase, just before each test.
counter = 10
test "first increment":
counter.inc
check counter == 11
test "independent reset":
# counter is 10 again because setup ran before this test
counter.inc
check counter == 11
# 3. Code here would run during Discovery.
# Use teardown for post-test cleanup.
teardown:
discard
Using Checkpoints for Debugging
The checkpoint macro is used for identifying which iteration of a loop failed by printing a message only on failure. It is useful for analyzing failures in complex loops or retry logic.
import unittest2
proc eventuallySucceeds(maxAttempts: int): bool =
# Logic that might fail a few times
false
suite "retry logic":
test "fails with descriptive checkpoint":
for i in 1 .. 3:
checkpoint "Attempt number " & $i
check eventuallySucceeds(i)
Dependency Injection
Using mocks or fakes allows you to test business logic without relying on external services like databases or APIs. This makes your tests faster and more deterministic.
import unittest2
type
Mailer = ref object of RootObj
RealMailer = ref object of Mailer
FakeMailer = ref object of Mailer
sentTo: seq[string]
method send(m: Mailer, userId: string) {.base.} =
discard
method send(m: RealMailer, userId: string) =
# Real external I/O in production
discard
method send(m: FakeMailer, userId: string) =
m.sentTo.add(userId)
proc activateUser(userId: string, mailer: Mailer): bool =
if userId.len == 0:
return false
mailer.send(userId)
true
suite "activateUser":
var fake: FakeMailer
setup:
fake = FakeMailer(sentTo: @[])
test "sends welcome mail for valid id":
check activateUser("u-123", fake)
check fake.sentTo == @["u-123"]
test "rejects empty user id":
check activateUser("", fake) == false
check fake.sentTo.len == 0