Nim Testing Guide: unittest2, Testament, and Doctests
Nim has three testing approaches: the standard library unittest module (simple but limited), unittest2 (modern replacement with better output), and testament (Nim's own compiler test tool used for its standard library). For application code, unittest2 is the recommended choice. Nim also supports doctests through the runnableExamples pragma.
Key Takeaways
Use unittest2 instead of the standard unittest. The stdlib unittest has a confusing API and poor failure output. unittest2 is a drop-in replacement with cleaner output and better macro-based assertions.
suite and test macros organize tests. Tests are organized with suite "name": containing test "name": blocks. Setup/teardown use setup: and teardown: inside suites.
check is the primary assertion macro. Unlike assertion functions, check generates a failure message showing the expression that failed: check x == 5 prints "5 == 5 is false".
runnableExamples adds doctests to proc documentation. Code in runnableExamples blocks is compiled and run as part of nim doc generation. It's Nim's equivalent of Python doctests.
Testament is the Nim compiler's own test tool — avoid it for application tests. Testament is powerful but designed for compiler testing scenarios. Use unittest2 for application and library code.
The Nim Testing Landscape
Nim offers multiple testing approaches:
unittest(stdlib): included in Nim, simple but limitedunittest2: community library, better output, drop-in replacementtestament: Nim compiler's internal test tool, powerful but complexrunnableExamples: built into Nim's doc system, tests in docstrings
For new projects, use unittest2 for test suites and runnableExamples for documented library functions.
unittest2 Setup
nimble install unittest2Or add to your .nimble file:
# myproject.nimble
requires "nim >= 2.0.0"
requires "unittest2 >= 0.2.0"Writing Tests with unittest2
# tests/test_math.nim
import unittest2
import ../src/math_utils
suite "Basic arithmetic":
test "addition":
check add(2, 3) == 5
check add(-1, 1) == 0
test "subtraction":
check subtract(10, 4) == 6
test "factorial":
check factorial(0) == 1
check factorial(5) == 120
test "factorial does not accept negative":
expect ValueError:
discard factorial(-1)The source file:
# src/math_utils.nim
proc add*(a, b: int): int = a + b
proc subtract*(a, b: int): int = a - b
proc factorial*(n: int): int =
if n < 0:
raise newException(ValueError, "factorial requires non-negative input")
if n <= 1: return 1
result = n * factorial(n - 1)Run with:
nimble test
<span class="hljs-comment"># or
nim c -r tests/test_math.nimThe check Macro
check is unittest2's primary assertion. It evaluates a boolean expression and prints the failing sub-expression:
suite "check macro examples":
test "value comparison":
let x = 42
check x == 42 # passes
# check x == 0 # fails: "x == 0 is false, x = 42"
test "string comparison":
let name = "Alice"
check name == "Alice"
check name.len == 5
test "multiple checks in one test":
let items = @[1, 2, 3, 4, 5]
check items.len == 5
check items[0] == 1
check 3 in items
check 99 notin itemsSuite Setup and Teardown
suite "Database operations":
var db: DatabaseConnection
setup:
# Runs before each test
db = newConnection("test_db")
db.createTables()
teardown:
# Runs after each test
db.dropTables()
db.close()
test "insert and retrieve":
db.insert("users", {"name": "Alice", "age": 30})
let user = db.findByName("users", "Alice")
check user.isSome()
check user.get()["name"] == "Alice"
test "delete removes record":
db.insert("users", {"name": "Bob", "age": 25})
db.delete("users", "name", "Bob")
let result = db.findByName("users", "Bob")
check result.isNone()Exception Testing
Use expect to verify that code raises a specific exception:
suite "Error handling":
test "parseint raises for invalid input":
expect ValueError:
discard parseInt("not-a-number")
test "index out of bounds raises":
let arr = [1, 2, 3]
expect IndexDefect:
discard arr[10]
test "custom exception raised":
expect InsufficientFundsError:
withdraw(account, 9999.0)Parametric Tests
Nim doesn't have built-in parametric tests, but you can use loops or templates:
suite "Parametric tests via loop":
let testCases = [
("SAVE10", 10.0),
("SAVE20", 20.0),
("INVALID", 0.0),
]
for (code, expected) in testCases:
test "discount for code " & code:
let discount = applyDiscount(100.0, code)
check abs(discount - expected) < 0.001runnableExamples (Doctests)
Add runnableExamples to proc documentation for executable examples:
proc parseInt*(s: string): int =
## Parses a string as an integer.
##
runnableExamples:
doAssert parseInt("42") == 42
doAssert parseInt("-7") == -7
# Implementation...
result = 0
for c in s:
if c notin '0'..'9' and not (c == '-' and result == 0):
raise newException(ValueError, "Invalid integer: " & s)
if c != '-':
result = result * 10 + (ord(c) - ord('0'))Run doctests:
nim doc --project src/mymodule.nim
# Runs all runnableExamples during doc generationrunnableExamples blocks must use doAssert (not assert) to produce clear error messages.
Mocking in Nim
Nim doesn't have a mature mock library. Use dependency injection and procedure variables:
# src/notifier.nim
type
SendProc* = proc(to: string, msg: string): bool
proc notifyUser*(userId: string, message: string, send: SendProc): bool =
let email = getUserEmail(userId)
if email.len == 0: return false
return send(email, message)# tests/test_notifier.nim
import unittest2
import ../src/notifier
suite "Notification":
var capturedTo: string
var capturedMsg: string
var sendCallCount: int
proc mockSend(to, msg: string): bool =
capturedTo = to
capturedMsg = msg
inc sendCallCount
return true
setup:
capturedTo = ""
capturedMsg = ""
sendCallCount = 0
test "notifies user with correct email":
discard notifyUser("u1", "Hello", mockSend)
check capturedTo == "u1@example.com"
test "sends correct message":
discard notifyUser("u1", "test message", mockSend)
check capturedMsg == "test message"
test "returns false for unknown user":
let result = notifyUser("unknown", "msg", mockSend)
check result == false
check sendCallCount == 0Running Tests with Nimble
Structure your project for Nimble test discovery:
myproject/
├── myproject.nimble
├── src/
│ └── myproject.nim
└── tests/
├── test_math.nim
└── test_parser.nimIn your .nimble file:
# myproject.nimble
task test, "Run all tests":
exec "nim c -r tests/test_math.nim"
exec "nim c -r tests/test_parser.nim"Or use a test runner script:
# Run all test files
<span class="hljs-keyword">for f <span class="hljs-keyword">in tests/test_*.nim; <span class="hljs-keyword">do
nim c -r <span class="hljs-string">"$f"
<span class="hljs-keyword">doneCI Integration
# .github/workflows/test.yml
name: Nim Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Nim
uses: jiro4989/setup-nim-action@v1
with:
nim-version: 2.0.2
- name: Install dependencies
run: nimble install -y
- name: Run tests
run: nimble testunittest vs. unittest2 vs. Testament
| unittest (stdlib) | unittest2 | testament | |
|---|---|---|---|
| Installation | Built-in | nimble install |
Nim compiler only |
check macro |
No (uses check proc) |
Yes (expression-aware) | No |
| Suite/setup | Basic | Clean | Complex |
| Failure output | Poor | Good | Excellent |
| Use case | Quick scripts | Application/library tests | Compiler tests |
| Recommended for? | Not recommended | ✓ Application code | Compiler/stdlib development |
Summary
Nim testing with unittest2:
suite "name":/test "name":organizes testscheck expressionis the primary assertion — shows failing sub-expressionssetup:andteardown:handle per-test lifecycleexpect ExceptionType:verifies exception raisingrunnableExamplesblocks add executable doctests to procedures- Mocking uses dependency injection with proc variables
Install unittest2 and start with your pure computation functions — Nim's functional style makes them easy to test in isolation.