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