Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Tip

Combine this with -d:unittest2Static=true to run regular test blocks at compile-time as well.

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