Skip to content

Motivation & decisions

Why brighttest is shaped the way it is — the problem, the options we weighed, and the decisions we made. If you just want to use it, jump to the Quick start.

The problem

Testing a Roku channel traditionally means the official rokudev/unit-testing-framework:

  • Device-only. Tests are packaged into the channel, sideloaded, launched over ECP, and results read back over telnet (port 8085). Slow, and awkward in CI.
  • No mocking, no coverage. You assert against real Roku objects — integration-style, not isolated units.
  • Unmaintained. Frozen since 2019.

We wanted three things — CI without physical devices, mocking & code coverage, and a fast feedback loop — for a plain BrightScript (.brs) codebase we did not want to rewrite.

Decision 1 — Don't build a new engine

A from-scratch engine is the obvious temptation and the wrong move:

  • A BrightScript-based runner still executes on a Roku, so it can't remove the device dependency.
  • A JavaScript-based engine means re-implementing a BrightScript interpreter — a huge, low-ROI effort.

Both roads reinvent mature, maintained tools. So we compose those instead.

Decision 2 — BrighterScript as the base

BrighterScript (bsc) is a compiler / transpiler / plugin host that works on plain .brs unchanged (the language is a superset). It gives us static validation, a build/deploy pipeline (roku-deploy), and — crucially — a plugin API other tools build on. It is not a runtime; it does not execute code.

Decision 3 — Rooibos as the test framework

Rooibos is a maintained rewrite of the official framework, shipped as a BrighterScript plugin. It provides mocking/stubbing/spies, code coverage (LCOV), @SGNode node-test scaffolding, and a describe/it authoring style. We standardize on Rooibos syntax as the single way to write tests.

Decision 4 — A headless lane for the fast loop

Rooibos's own runner is SceneGraph-scene based, so out of the box it only runs on a device (its CLI requires --host/--password). To get device-free runs we add a small headless driver that reuses Rooibos's own compiled assertions but replaces the scene runner, executing suites on the brs-node BrightScript simulator. Result: one spec file runs both lanes. (See Architecture.)

The boundary we accept

There is no desktop Roku emulator — Roku testing runs on real hardware or a simulator. But the device-only boundary is much narrower than it first appears:

  • Coverage runs headless. brighttest --coverage produces real LCOV on the simulator with no device attached. (The device lane produces coverage too, and serves as the reference.)
  • @SGNode node tests run headless. Real SceneGraph nodes and their onChange observer cascades execute on the simulator — in the default and --coverage lanes — made faithful by a handful of simulator fidelity fixes and policed by --cross-check.
  • Only behavior tied to real wall-clock timing is genuinely device-only — animations played out over frames, Task-node I/O, live remote input. The device stays the fidelity reference; --cross-check proves the fast lane matches it, and a test that truly needs hardware can be marked @deviceOnly.

What we evaluated and rejected

OptionVerdict
New BrightScript engineRejected — can't escape the device; Rooibos already is the maintained rewrite.
New JavaScript engineRejected — re-implements an interpreter that already exists (brs-node).
Rooibos-only (device for everything)Rejected as default — loses the fast, device-free loop.
@rokucommunity/brs as the headless interpreterRejected — its parser can't handle the Rooibos runtime. See Troubleshooting.

Outcome

A thin tool over a mature stack: BrighterScript + Rooibos + brs-node, with a headless driver that unifies authoring. It was validated end-to-end against a large production BrightScript codebase — both lanes green on the same specs, with coverage + LCOV produced headless and fidelity confirmed on real hardware via --cross-check.