1. Your first test
We'll write a tiny piece of BrightScript, test it, watch the test pass, deliberately break it to see a failure, and fix it. By the end you'll understand the whole loop.
Step 1 — Some code to test
Create source/mathutils.brs with a plain function. Nothing special — this is ordinary BrightScript that your app could use anywhere:
' source/mathutils.brs
function addNumbers(a as integer, b as integer) as integer
return a + b
end functionWhy source/?
Roku only compiles BrightScript under source/ and components/. Both your code and your test files must live under one of those. We'll put tests in source/tests/.
Step 2 — A test for it
Create source/tests/MathUtils.spec.bs. (Note the .bs extension and the .spec in the name — the default pattern is **/*.spec.bs.)
namespace tests
@suite("math utils")
class MathUtilsTests extends rooibos.BaseTestSuite
@describe("addNumbers")
@it("adds two positive numbers")
function _()
result = addNumbers(2, 3)
m.assertEqual(result, 5)
end function
end class
end namespaceDon't worry about every keyword yet — the next page dissects it line by line. For now: the @it function calls your code and asserts the result equals 5.
Step 3 — Run it (headless)
From your project root:
npx brighttestYou should see something like:
brighttest: headless lane
✓ addNumbers > adds two positive numbers
====================================================
brighttest (headless): 1 passed, 0 failed
====================================================That ran with no Roku device — on a BrightScript simulator in Node, in well under a second. This is your everyday feedback loop.
Step 4 — Watch it fail
A test you've never seen fail is a test you don't trust. Change the assertion to expect the wrong answer:
m.assertEqual(result, 6) // wrong on purposeRun again:
✗ addNumbers > adds two positive numbers — expected "5 (Integer)" to equal "6 (Integer)"
brighttest (headless): 0 passed, 1 failedThe command also exits non-zero, which is how CI knows the build is broken. Notice the message shows both the actual value (5) and what you expected (6) — that's what makes failures easy to diagnose.
Now put it back to 5 and confirm it passes again.
Step 5 — (Optional) coverage, and running on a device
Coverage doesn't need a device — the --coverage lane runs headless and writes LCOV:
npx brighttest --coverage --lcov coverage/lcov.infoThe same file also runs on real hardware if you have a Roku in developer mode. You don't need it for day-to-day work — headless is the fast loop and even runs SceneGraph node tests — but the device lane is the fidelity reference, and --cross-check diffs the two to keep the fast lane honest:
npx brighttest --cross-check --host <roku-ip> --password <dev-pw>The loop you'll repeat forever
That's it — you've written and run a test. Next, let's understand exactly what each part of that spec file does.