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 --coverageproduces real LCOV on the simulator with no device attached. (The device lane produces coverage too, and serves as the reference.) @SGNodenode tests run headless. Real SceneGraph nodes and theironChangeobserver cascades execute on the simulator — in the default and--coveragelanes — 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-checkproves the fast lane matches it, and a test that truly needs hardware can be marked@deviceOnly.
What we evaluated and rejected
| Option | Verdict |
|---|---|
| New BrightScript engine | Rejected — can't escape the device; Rooibos already is the maintained rewrite. |
| New JavaScript engine | Rejected — 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 interpreter | Rejected — 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.