4.5.6 Unit Testing Pure Starlark Utilities

recommended

Not every Starlark test needs a target under test. If a helper accepts ordinary values and returns ordinary values, test it directly. This is the cheapest proof layer: no rule context, configured target, provider, or action is needed. rules_testing supplies a unit-test front end and Truth-style assertions for this purpose.1

Test the value contract

Suppose a private helper normalizes a list of extensions. Give the test a few representative lists and assert on the returned sequence. Group focused test functions into the framework's Bazel test target or suite. Keep test implementation functions private so tooling can warn when one is omitted from the suite's explicit list. Put each suite in its own package when generated target names could otherwise collide.1

This style builds on the function and value model from 4.1 Macro Design & Rule-Authoring Starlark. The test list is still ordinary explicit Starlark, not discovery by reflection.

For example, extension_tests.bzl can contain both the pure helper and its focused test:

load("@rules_testing//lib:test_suite.bzl", "test_suite")

def _normalize_extensions(values):
    return [
        value[1:].lower() if value.startswith(".") else value.lower()
        for value in values
    ]

def _normalize_extensions_test(env):
    actual = _normalize_extensions([".Bzl", "STARLARK"])
    env.expect.that_collection(actual).contains_exactly(
        ["bzl", "starlark"],
    ).in_order()

def extension_test_suite(name):
    test_suite(
        name = name,
        basic_tests = [_normalize_extensions_test],
    )

The package's BUILD.bazel instantiates the exported suite:

load(":extension_tests.bzl", "extension_test_suite")

extension_test_suite(name = "extension_tests")

Run it like any other Bazel test:

bazel test //tools/extensions:extension_tests

Success is the ordinary PASSED test result. If the returned list differs, the Truth assertion reports the actual and expected collection rather than leaving the test to construct a message with fail().1

Raw fail() calls work, but fluent subjects produce failures with the actual value, expected value, and assertion context. Use built-in subjects for common values. Define a custom subject when a provider-like domain value needs the same structured assertions across many tests.1

Escalate when the question changes

The boundary is the evidence you need:

This ladder prevents a tiny string transformation from requiring a synthetic rule while also preventing a value-level test from pretending to verify Bazel analysis.

For a published ruleset, keep these suites beside the code they check and make their public/private boundary follow 4.11.1 Ruleset Layout & Public Entry Points.

key takeaway

Test pure Starlark utilities directly with small test functions and structured Truth assertions. Add custom subjects for recurring domain values, keep suite assembly hygienic, and escalate to analysis or integration tests only when the claim requires configured targets or a real workspace.

Footnotes

  1. rules_testing repository map — unit-test setup, Truth assertions, custom subjects, suite hygiene, and the boundary with analysis tests. 1 2 3 4