Writing Tests with unittest
Most test frameworks are a runtime affair: a discovery harness scans your filesystem, a runner reflects over modules at startup, decorators register callbacks, and a command-line tool (pytest, go test, cargo test) bootstraps the whole thing. Dragon does less. A test file is just a Dragon program - top-level statements that build a suite and run it. You launch it the same way you launch anything else:
dragon run test/dr/test_math.dr
There is no special test subcommand, no magic file naming the compiler cares about, no if __name__ == "__main__" guard. The file you hand to dragon run is the program, and the program's last line is the call that runs the tests. If that suite fails, the process exits non-zero - so CI, shell scripts, and CTest all gate on it for free.
The framework itself is unittest, and it deliberately mirrors Python's unittest: you subclass TestCase, write test_* methods, and call the assertion helpers. If you've written a Python unittest.TestCase, you already know the shape. The whole framework is itself written in Dragon (stdlib/unittest.dr) - there is no C++ test runtime underneath it.
This chapter covers writing and running a suite; the full assertion vocabulary and the conventions for organizing tests across a project are the subject of Assertions and Test Organization.
The first test
A test suite is a class that extends TestCase. Each method whose name begins with test_ is one test:
from unittest import TestCase, main
class MathTests(TestCase) {
def test_addition() {
self.assertEqual(2 + 2, 4)
}
def test_ordering() {
self.assertTrue(10 > 3)
self.assertGreater(10, 3)
}
}
main([MathTests()])
Run it:
dragon run test_math.dr
ok: test_addition
ok: test_ordering
Ran 2 test(s); failures: 0, errors: 0
OK
Three things are worth pinning down right away, because they are the whole model:
- You subclass
TestCasewith.drconstructor syntax. Test methods take no parameters except the implicitself, and you don't write adef()constructor unless you need one -TestCasealready provides it. main([...])takes a list of instances, not classes. You writeMathTests(), notMathTests. Until module-level class enumeration lands, the caller hands the runner alist[TestCase]explicitly - no decorator magic, no filesystem discovery. It's more typing, but it's also fully debuggable: you see exactly which suites run.- The last line runs the program.
main([MathTests()])is an ordinary function call at the top level of the file. Delete it and the file compiles, runs, and prints nothing - the tests are defined but never invoked. This is the same no-magic-mainrule that governs every Dragon program.
The assertions used here - assertEqual, assertTrue, assertGreater - are three of the helpers TestCase provides; the next chapter catalogs them all.
What a failure looks like
When an assertion fails, the runner prints a FAIL: line naming the test and the reason, but keeps going - one failing assertion does not abort the suite. Take a deliberately broken test:
from unittest import TestCase, main
class BrokenTests(TestCase) {
def test_ok() {
self.assertEqual(2 + 2, 4)
}
def test_broken() {
self.assertEqual(2 + 2, 5)
}
}
main([BrokenTests()])
FAIL: test_broken: assertEqual: 4 != 5
ok: test_ok
Ran 2 test(s); failures: 1, errors: 0
FAILED
The process exits with code 1. Two distinctions the summary draws:
- A failure is a missed assertion (
failures:). - An error is an uncaught exception thrown inside a test body - the runner catches it, reports
ERROR: <test>: <message>, and counts it undererrors:. A bug in the test or the code under test surfaces as an error, not a crash that takes the whole run down with it.
The final line is OK if both counts are zero, otherwise FAILED, and the process exits 0 or 1 to match. That non-zero exit is the contract the rest of your tooling hangs off - CI, a shell && chain, or CTest all read it directly, with nothing to parse.
Tests run in the order
dir()reports the methods, which isalphabetical - not definition order. Don't write tests that depend on
running in a particular sequence; each
test_*should stand alone.
Multiple suites in one file
main takes a list, so a single file can carry several suites and run them together:
import unittest
from unittest import TestCase
class StringTests(TestCase) {
def test_upper() { self.assertEqual("ab".upper(), "AB") }
}
class ListTests(TestCase) {
def test_len() { self.assertEqual(len([1, 2, 3]), 3) }
}
unittest.main([StringTests(), ListTests()])
Both the from unittest import main and the qualified unittest.main(...) forms work - they're the same function. Use whichever reads better; this book favours the short import.
That is the whole runner: a file, a TestCase subclass, and one line to kick it off. What remains is the vocabulary of assertions and the conventions for organizing tests at project scale - both covered next, in Assertions and Test Organization.
