magus v0.3.0 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
4 min read

Testing

Buzz builds testing into the language, as Go does: a test block is a first-class construct. magus adds two small libraries that make those blocks easier to write. assert provides value-aware matchers; suite groups stateful tests. magus registers both as extensions alongside Buzz's standard library.

What to test, and what not to

Test the code that carries logic: standalone spells, and Buzz modules imported into a magusfile. The docs project's magusfile imports docs/render.buzz, a static-site generator, and that generator earns its tests.

Do not write tests for the magusfile itself.

A magusfile is declarative build configuration. Keep it thin: wire targets together and push logic elsewhere. When a magusfile grows complex enough to want tests, move that logic into a spell or a sibling module and test it there. A magusfile wires your build together, so testing one means testing your configuration. Keep magusfiles thin enough that the question never comes up.

Writing a test

A test block runs when you execute the file with -t. Inside it, call assert matchers; a failed assertion raises and fails the test.

import "assert";

test "collectAlias trims slashes and records every alias" {
    final s = mut Site{};
    s.collectAlias("/old-path/", "new-path/");

    // Buzz's == is reference identity for maps, so compare by value with assert.
    assert\equal(s.aliasTarget, {"old-path": "new-path/"}, "alias recorded, slashes trimmed");
}

Run the tests:

magus buzz -t --embedded render.buzz   # a single file
magus run buzz-test                    # the project's test target

The assert module

Every matcher raises on failure, the same contract as the built-in std\assert, with value-aware checks on top. Buzz has no optional or variadic parameters, so each matcher takes a trailing message (pass "" for none; a short label makes failures easier to read).

Matcher Passes when
equal(got, want, message) got is structurally (deeply) equal to want
notEqual(got, want, message) got is not deeply equal to want
isTrue(got, message) / isFalse(got, message) got is true / false
isNull(got, message) / notNull(got, message) got is / is not null
contains(container, item, message) a str holds a substring, a list holds an element (by deep equality), or a map holds a key
len(container, want, message) a str, list, or map has exactly want elements
isEmpty(container, message) a str, list, or map is empty
greater(a, b, message) / less(a, b, message) two numbers or two strings order that way
throws(fn, message) / doesNotThrow(fn, message) calling fn does / does not raise
fail(message) Always fails the test

Reach for equal most often. == compares identity for maps, lists, and objects, so {a: 1} == {a: 1} is false; assert\equal compares by value, recursing to any depth and ignoring map key order.

assert\skip(message) stops the current test and marks it skipped instead of failed (Go's t.Skip). Use it for a case that cannot run in the current environment. The runner reports skipped tests apart from failures:

test "reads the platform keychain" {
    if (os\env("CI") != null) {
        assert\skip("no keychain in CI");
    }
    assert\notNull(readKey(), "key present");
}

The suite module

Reach for a suite when several tests share setup or accumulated state. A suite runs every case and reports all failures at the end; its matchers are soft, so a failed check records the failure and the test keeps going. You run a suite as a plain script rather than with -t: build the suite, run cases with it, then call summary.

import "suite";

final s = suite\new(
    setupAll: null,
    setupEach: fun () > void { /* fresh fixture per test */ },
    teardownAll: null,
    teardownEach: null
);

s.it("parses a release line", fun () > void {
    s.equal(parseVersion("## [1.2.0] - 2026-01-01"), "1.2.0", "version extracted");
    s.notNull(s, "suite is live");
});

s.summary();   // prints results; exits non-zero if any test failed

The soft matchers mirror assert's core (equal, notEqual, isTrue, isFalse, isNull, notNull, fail) as methods on the suite. The lifecycle hooks (setupAll, setupEach, teardownAll, teardownEach) run around the cases; pass null to skip one.

Per-test controls

Go's testing package hangs its features off the *testing.T passed to each test. A test block has no such handle; a Suite is that handle, so it carries the same per-test controls, callable from inside an it body:

Method Go analog Effect
s.skip(message) t.Skip Marks the test skipped and stops its body
s.fatal(message) t.Fatal Records a failure and stops the body, for a broken precondition where the soft matchers would keep going
s.cleanup(fn) t.Cleanup Registers a cleanup run after the test in reverse order, whether it passed or failed
s.log(message) t.Log Buffers a line printed only if the test fails
s.name() t.Name The name of the running test
s.it("reads the fixture", fun () > void {
    if (os\env("CI") == null) {
        s.skip("needs the CI fixture");
    }

    final handle = openFixture();
    s.cleanup(fun () > void { handle.close(); });   // always runs

    s.log("fixture at {handle.path}");              // shown only on failure
    s.equal(handle.read(), "expected", "contents");
});
testingbuzzassertsuitespellsconventions
Last updated (e0463131)
Earlier changes on this page (1)

Full history ↗ · Blame source ↗

Glossary

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. See the module reference.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.