Testing TypeRB applications

TypeRB provides a portable test API in trb/std/test. The same test source runs through the Go, Ruby, and TypeScript backends.

Colocate unit tests with the source they exercise below the configured sourceDir. For calculator.trb, use calculator_test.trb in the same directory:

calculator.trb
calculator_test.trb

The matching filename is the default: <filename>.trb and <filename>_test.trb. A large unit suite may use additional concern-specific files beside the same source, such as calculator_overflow_test.trb.

Choose placement by the behavior the test guarantees. A test of one module's contract stays beside that module even if constructing its input uses several other modules or an external process. Tests of a complete feature across several components may live in a feature directory such as src/tests/orders/checkout_test.trb. System tests belong with the subsystem whose end-to-end contract they verify. Every TypeRB test file still uses the _test.trb suffix, and trb test discovers it recursively below sourceDir.

Extract reused fixtures and execution helpers into ordinary helper modules rather than importing one test file from another. A project may group shared helpers in testing/; keep test-case registration in test files and application code independent of test helpers. The names tests/ and testing/ are project conventions, not reserved directories: a helper without the _test.trb suffix is an ordinary module under the language rules.

Keep unit tests beside their source when reorganizing a large directory. Move both together by responsibility rather than relocating unit tests only to reduce the directory's file count. Add tests for observable contracts and meaningful boundaries; one test file per source is a placement convention, not a requirement to create a test for every implementation file.

For example, calculator.trb can define the function under test:

def add(left: Integer, right: Integer): Integer
	return left + right
end

A test file uses named imports for the explicit suite, case, and assertion helpers:

import { add } from calculator
import { describe, expect, test } from trb/std/test

describe("Calculator") do
	test("adds numbers") do
		expect(add(1, 2)).to_equal(3)
	end

	describe("negative values") do
		test("preserves the sign") do
			expect(add(0, -2)).to_equal(-2)
		end
	end
end

Run every project test with:

trb test

Select a suite or case by its full display name:

trb test -t "Calculator / negative values$"

Pass files or directories positionally to select their tests. Directories are searched recursively, and multiple paths form a union:

trb test src/calculator_test.trb
trb test src/domain src/application/orders_test.trb
trb test src/domain/*_test.trb

The last example relies on shell expansion; trb test does not interpret a quoted glob. Only selected test files are compiled during a focused run, while the complete set of production files remains available. Editors combine the file and full name so identically named suites in different modules remain independent.

Failures return a nonzero process status and point to the original .trb assertion. --reporter json emits one JSON object per line for editors and other tools.

The initial assertion surface is:

  • expect(actual).to_equal(expected)
  • expect(actual).to_not_equal(expected)
  • expect(actual).to_be_true()
  • expect(actual).to_be_false()
  • expect(actual).to_be_nil()
  • expect_ok(actual_result)
  • expect_err(actual_result)

Equality assertions compare portable values structurally across backends, including Arrays, Hashes, records, and value-carrying enums.

describe() contains nested describe() and test() declarations. A test() body is ordinary TypeRB code, so shared setup is an explicit helper function and data-driven tests use an Array with each:

describe("Parser") do
	test("accepts valid integers") do
		["0", "12", "-7"].each do |source|
			expect(source.to_i()).to_not_equal(999)
		end
	end
end

Recoverable operations return ordinary Result values. Use try only when a helper function itself returns a compatible Result, and use catch when a test can recover with a value. Result values already support structural equality, so an exact result can be asserted directly. Use expect_ok() or expect_err() when the payload is the subject of later assertions:

expect(load_user("missing")).to_equal(UserResult::Err(UserError::NotFound("missing")))

user := expect_ok(load_user("ada"))
expect(user.name).to_equal("Ada")

expect(expect_err(load_user("invalid"))).to_equal(UserError::Invalid("invalid"))

expect_ok() returns the Ok value and fails the current test if it receives Err. expect_err() returns the Err value and fails if it receives Ok. Both failures point to the helper call. An exhaustive case remains available when the test needs branch-specific control flow; these helpers do not add an implicit Result boundary.

There is no implicit setup inheritance, dedicated table-test syntax, fixture registry, or mock-generation magic. Tests use ordinary constructors, interfaces, and explicit fake implementations. Higher-level testing packages can build on this portable core without changing the runner protocol.

The VS Code extension discovers the same suite hierarchy through trb lsp, shows it in Test Explorer, and supports running a project, suite, or individual case. Go projects can also debug those selections with ordinary .trb breakpoints, stepping, and variable inspection through Delve. Test CodeLens actions use that same native Testing API and debugger integration.

TypeScript process tests currently require the Bun or Node runtime. Browser test execution is staged separately because it needs a browser host rather than the process runner used by trb test.