name: nim-testing description: Write and run deterministic Nim tests, including isolated test files, expected-exception checks, multi-configuration builds, and sanitizer integration. Use when setting up a Nim test suite, testing failure behavior, running tests across debug/release/danger modes, or adding AddressSanitizer support.
Nim Testing
Write deterministic Nim tests with direct assertions, isolated test files, multi-configuration runs, and optional AddressSanitizer checks.
Extended examples and CI workflows live in references/.
Rules
Use block-based tests with doAssert
Import std/assertions. Use block scopes with doAssert and doAssertRaises:
import std/assertions
block add_basic:
doAssert add(1, 2) == 3, "add should sum positive integers"
block greet_empty:
doAssert greet("") == "hello "
block parse_bad_input:
doAssertRaises ValueError:
discard parseThing("")
doAssert raises AssertionDefect on failure. For catchable exceptions, doAssertRaises passes only when the requested exception type is raised; otherwise the test exits non-zero.
doAssert vs assert
doAssert runs in all build modes. Plain assert is compiled out in -d:danger. Use doAssert in tests.
Use doAssertRaises for Expected Exceptions
Use doAssertRaises for expected exceptions:
import std/assertions
block parse_bad_input:
doAssertRaises ValueError:
discard parseThing("")
Project layout
project/
src/
mylib.nim
tests/
config.nims # shared compiler switches
tester.nim # central test runner (auto-discovers t*.nim)
tbasic.nim
tedge.nim
terrors.nim
Test files use t prefix: tbasic.nim, tedge.nim, terrors.nim, tintegration.nim.
tests/config.nims
switch("path", "$projectdir/../src")
The compiler loads this config automatically when compiling files in tests/.
tests/tester.nim
import std/os
proc fatal(msg: string) = quit "FAILURE " & msg
proc exec(cmd: string) =
echo "Running: ", cmd
if execShellCmd(cmd) != 0: fatal cmd
let testDir = getCurrentDir() / "tests"
for f in walkFiles(testDir / "t*.nim"):
let name = f.extractFilename
if name == "tester.nim":
discard
else:
exec "nim c -r " & quoteShell(testDir / name)
echo "All test files completed."
Run from project root: nim c -r tests/tester.nim
New test files are auto-discovered — no runner edits needed.
Shared Test Code
Keep test files self-contained when practical. If setup is repeated, extract domain helpers such as sample builders, temp-file setup, or fixture data.
Workflow
Set up layout. Create
src/,tests/, andtests/config.nims.Write test files. Name files with
tprefix. Useblock,doAssert, anddoAssertRaises.Create the runner. Add
tests/tester.nimwith the auto-discover pattern.Run all configurations:
nim c -r tests/tester.nim nim c -d:release -r tests/tester.nim nim c -d:danger -r tests/tester.nimRun with ASan if the project uses unsafe constructs. See "AddressSanitizer" below.
Set up CI. See
references/ci_github_actions.md.
Multi-configuration testing
| Mode | Overflow checks | Stack traces (file:line) |
|---|---|---|
default / -d:debug |
Yes | Full |
-d:release |
Yes | Raising frame only |
-d:danger |
No | Raising frame only |
- Overflow checks: Disabled in danger. Use
when defined(danger)guards. - Stack traces: Release and danger show only the raising frame. Add
--lineTrace:onto restore full traces. assert: Compiled out in danger. UsedoAssert.- Expected exceptions: Use
doAssertRaises.
AddressSanitizer
nim c \
--passC:"-fsanitize=address -fno-omit-frame-pointer" \
--passL:"-fsanitize=address -fno-omit-frame-pointer" \
-g -d:noSignalHandler -d:useMalloc \
-r tests/tester.nim
--passC/--passL: Both required.-g: Embeds debug info for Nim source locations in reports.-d:noSignalHandler: Lets ASan report directly instead of Nim's signal handler.-d:useMalloc: Uses C'smallocso ASan tracks every allocation.
Windows (MSVC): nim c --cc:vcc --passC:"/fsanitize=address" -r tests/tester.nim
Sanitizer config in tests/config.nims
switch("path", "$projectdir/../src")
when defined(addressSanitizer):
switch("debugger", "native")
switch("define", "noSignalHandler")
switch("define", "useMalloc")
when defined(windows):
switch("passC", "/fsanitize=address")
else:
switch("passC", "-fsanitize=address -fno-omit-frame-pointer")
switch("passL", "-fsanitize=address -fno-omit-frame-pointer")
Then: nim c -d:addressSanitizer -r tests/tester.nim
Common Mistakes
| Mistake | Why it is wrong |
|---|---|
Using assert instead of doAssert |
assert is compiled out in danger. Use doAssert. |
Relying on OverflowDefect without when defined(danger) |
Never raised in danger mode. |
Running ASan without -d:useMalloc |
Nim's default allocator is not intercepted by ASan. |
Running ASan without -d:noSignalHandler |
Nim's signal handler intercepts SIGSEGV before ASan reports. |
Using only --passC without --passL for ASan |
The sanitizer runtime must be linked. |
References
references/block_test_pattern.md— Full worked example with project layout, test files, and runnerreferences/ci_github_actions.md— GitHub Actions CI workflow for Linux, macOS, and Windows