boxlang-testing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited boxlang-testing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
BoxLang is the AI-native software productivity platform for building, modernizing and running applications, with developers and AI agents working together.
TestBox is the standard testing framework for BoxLang. It supports two styles:
describe(), it(), feature(), story(), given()/when()/then()test*() methods, setup()/tearDown()Both styles use the same assertions/expectations library and MockBox for mocking.
A test bundle is a BoxLang class (.bx file) that contains your tests. Name files with Test or Spec suffix by convention: UserServiceSpec.bx, OrderServiceTest.bx.
// tests/specs/UserServiceSpec.bx
class extends="testbox.system.BaseSpec" {
function run() {
describe( "UserService", () => {
it( "can greet a user", () => {
expect( "hello" ).toBe( "hello" )
} )
} )
}
}Extending testbox.system.BaseSpec is optional but recommended: it enables IDE introspection, direct web runner execution, and faster test loading.
| Variable | Purpose |
|---|---|
$mockbox | MockBox instance for creating mocks |
$assert | Assertions library |
$utility | Utility helpers |
$customMatchers | Custom matcher registry |
$testID | Unique ID for the bundle |
$debugBuffer | Debug output buffer |
class extends="testbox.system.BaseSpec" {
function run() {
describe( "Calculator", () => {
it( "can add two numbers", () => {
var calc = new Calculator()
expect( calc.add( 1, 2 ) ).toBe( 3 )
} )
it( "throws on divide by zero", () => {
expect( () => new Calculator().divide( 10, 0 ) ).toThrow()
} )
} )
}
}| Function | Alias / Purpose |
|---|---|
describe( title, body ) | Standard suite |
feature( title, body ) | Feature-level suite |
story( title, body ) | Story-level suite |
scenario( title, body ) | Scenario in feature/story |
given( title, body ) | Given block (GWT) |
when( title, body ) | When block (GWT) |
then( title, body ) | Then block / alias for it() |
feature( "User authentication", () => {
scenario( "valid credentials", () => {
given( "a registered user", () => {
when( "they log in with correct password", () => {
then( "they should receive a JWT token", () => {
var token = authService.login( "[email protected]", "secret" )
expect( token ).notToBeEmpty()
} )
} )
} )
} )
} )// it() is the primary spec block
it( "does something", () => {
expect( true ).toBeTrue()
} )
// then() is an alias for it(): title arg is "then" instead of "title"
then( "something should happen", () => {
expect( result ).notToBeNull()
} )Life-cycle methods control setup and teardown around specs.
class extends="testbox.system.BaseSpec" {
// Runs ONCE before all specs in the bundle
function beforeAll() {
variables.db = new Database()
variables.db.connect()
}
// Runs ONCE after all specs in the bundle
function afterAll() {
variables.db.disconnect()
directoryDelete( "/tests/tmp", true )
}
function run() { ... }
}beforeEach / afterEach / aroundEach)describe( "UserService", () => {
beforeEach( ( currentSpec, data ) => {
variables.service = new UserService()
} )
afterEach( ( currentSpec, data ) => {
variables.service = javaCast( "null", "" )
} )
aroundEach( ( spec, suite, data ) => {
// wraps the spec; you MUST call spec.body() yourself
transaction action="begin" {
arguments.spec.body()
transaction action="rollback"
}
} )
it( "can find a user by id", () => {
var user = variables.service.findById( 1 )
expect( user ).toHaveKey( "name" )
} )
} )beforeEachwalks down the nesting tree (outer → inner).afterEachwalks up the tree (inner → outer).
Annotate any function to hook into the life-cycle without overriding named methods. Useful for base test classes:
class extends="testbox.system.BaseSpec" {
/**
* @beforeAll
*/
function initAppContext() {
// Called once before all specs; discovered up the inheritance chain
}
/**
* @aroundEach
*/
function wrapInTransaction( spec, suite ) {
transaction action="begin" {
try {
arguments.spec.body()
} finally {
transaction action="rollback"
}
}
}
}Pass data to life-cycle methods so dynamic values are captured correctly in loops:
var fixtures = [ "admin", "guest", "editor" ]
for ( var role in fixtures ) {
describe( "Role: #role#", () => {
beforeEach(
data = { roleName = role },
body = ( currentSpec, data ) => {
variables.user = userFactory.create( data.roleName )
}
)
it(
title = "has correct permissions",
data = { role = role },
body = ( data ) => {
expect( variables.user.getRole() ).toBe( data.role )
}
)
} )
}@DisplayName "OrderService Tests"
class extends="testbox.system.BaseSpec" {
// Runs ONCE before all tests
function beforeTests() {
application.dbPool = createDBPool()
}
// Runs ONCE after all tests
function afterTests() {
structClear( application )
}
// Runs before EACH test method
function setup() {
variables.service = new OrderService()
}
// Runs after EACH test method
function tearDown() {
variables.service = javaCast( "null", "" )
}
// Any function with "test" prefix is a test
function testCalculateTotalWithDiscount() {
var total = variables.service.calculateTotal( items=[], discount=0.1 )
$assert.isEqual( total, 0 )
}
// Or use @test annotation for any naming
@DisplayName "It should reject empty orders"
@test
function itRejectsEmptyOrders() {
expect( () => variables.service.place( [] ) ).toThrow( type="EmptyOrderException" )
}
// Skip a specific test
function testSkipped() skip {
$assert.fail( "should not run" )
}
}Use expect( actual ) to start a fluent assertion chain. Prefix any matcher with not for the negative version.
// Equality
expect( result ).toBe( expected )
expect( result ).notToBe( expected )
expect( "Hello" ).toBeWithCase( "Hello" )
// Boolean
expect( isActive ).toBeTrue()
expect( isDeleted ).toBeFalse()
// Null
expect( value ).toBeNull()
expect( value ).notToBeNull()
// Type checks
expect( items ).toBeArray()
expect( config ).toBeStruct()
expect( id ).toBeNumeric()
expect( dob ).toBeDate()
expect( obj ).toBeComponent()
expect( obj ).toBeInstanceOf( "com.example.MyClass" )
expect( obj ).toBeTypeOf( "uuid" ) // uses isValid()
// Size / emptiness
expect( [] ).toBeEmpty()
expect( items ).notToBeEmpty()
expect( items ).toHaveLength( 3 )
expect( "Hello" ).toHaveLength( 5 )
// Struct keys
expect( user ).toHaveKey( "name" )
expect( config ).toHaveDeepKey( "database.host" )
// Strings / arrays
expect( message ).toMatch( "^Error" ) // regex, case-insensitive
expect( message ).toMatchWithCase( "ERROR" ) // regex, case-sensitive
expect( items ).toInclude( "admin" ) // case-insensitive
expect( items ).toIncludeWithCase( "Admin" ) // case-sensitive
// Numbers / dates
expect( value ).toBeGT( 0 )
expect( value ).toBeGTE( 1 )
expect( value ).toBeLT( 100 )
expect( value ).toBeLTE( 99 )
expect( value ).toBeBetween( 1, 100 )
expect( score ).toBeCloseTo( 3.14, 0.01 )
// Exceptions (ALWAYS use a closure)
expect( () => service.doThing() ).toThrow()
expect( () => service.doThing() ).toThrow( type="MyException" )
expect( () => service.doThing() ).toThrow( regex="not found" )
expect( () => service.doThing() ).toThrow( type="MyException", regex="not found" )// Multiple matchers on the same value
expect( result )
.notToBeNull()
.toBeStruct()
.toHaveKey( "id" )
.toHaveKey( "name" )expectAll)Test every element of an array or struct at once:
expectAll( users ).toHaveKey( "name" )
expectAll( scores ).toBeGT( 0 )
expectAll( [ true, true, true ] ).toBeTrue()
expectAll( { a: 2, b: 4, c: 6 } ).toSatisfy( (x) -> x mod 2 == 0 )// Preferred: closure-based
it( "throws on invalid input", () => {
expect( () => {
service.process( javaCast( "null", "" ) )
} ).toThrow( type="InvalidArgumentException" )
} )
// xUnit annotation style
function testThrowsOnNull() expectedException="InvalidArgumentException" {
service.process( javaCast( "null", "" ) )
}$assert)The traditional assertions library available via $assert in every bundle:
$assert.isTrue( condition, [message] )
$assert.isFalse( condition, [message] )
$assert.isEqual( expected, actual, [message] )
$assert.isNotEqual( expected, actual, [message] )
$assert.isNull( value, [message] )
$assert.isNotNull( value, [message] )
$assert.isEmpty( value, [message] )
$assert.isNotEmpty( value, [message] )
$assert.includes( collection, needle )
$assert.notIncludes( collection, needle )
$assert.includesWithCase( collection, needle )
$assert.throws( closure, [type], [regex], [message] )
$assert.fail( [message] )
// Dynamic assertion methods (BoxLang only)
assertIsTrue( condition )
assertIsEqual( expected, actual )
assertIsNull( value )
// Any $assert method can be called as assert{MethodName}()Prefix any suite/spec function with x OR use the skip argument:
// Prefix approach: skip entire suite
xdescribe( "Slow integration tests", () => { ... } )
// Prefix approach: skip single spec
xit( "is temporarily disabled", () => { ... } )
// skip argument: boolean or closure
it( title="runs only on production", skip=!isProduction(), body=() => { ... } )
describe( title="Lucee only", skip=() => !structKeyExists( server, "lucee" ), body=() => {
it( "uses Lucee ORM", () => { ... } )
} )
// skip() method inside spec: programmatic
it( "can run on multiple engines", () => {
if ( !server.keyExists( "boxlang" ) ) {
skip( "BoxLang-only feature" )
}
// ... rest of spec
} )Prefix with f or use focused=true: ONLY focused specs/suites run:
// Focus a single spec
fit( "is the only spec that runs", () => {
expect( true ).toBeTrue()
} )
// Focus a suite (all children run)
fdescribe( "Critical path", () => {
it( "step one", () => { ... } )
it( "step two", () => { ... } )
} )Warning: Never commit focused tests: CI will only run those specs.
MockBox is built into TestBox. Helper methods are available in every bundle.
// Create a mock of a class (real methods unless overridden)
var mockService = createMock( "services.UserService" )
// Create an empty mock (ALL methods removed: you mock everything)
var mockRepo = createEmptyMock( "repositories.UserRepository" )
// Decorate an existing instance with mocking capabilities
var realService = new services.UserService()
prepareMock( realService )
// Chainable setup
var mockUser = createMock( "model.User" )
.$( "isActive", true )
.$( "getRole", "admin" )
.$( "getName", "Luis Majano" )$()// Return a single value
mockRepo.$( "findById", { id: 1, name: "Luis" } )
// Return nothing (void)
mockRepo.$( "deleteById" )
// Throw an exception
mockRepo.$( "findById",
throwException = true,
throwType = "NotFoundException",
throwMessage = "Record not found"
)
// Use a callback for dynamic responses
mockRepo.$( method="findById", callback=( id ) => {
if ( id == 1 ) return { id: 1, name: "Luis" }
return javaCast( "null", "" )
} )$results()Returns values in sequence, cycling when exhausted:
// First call → true, second call → false, third → true (repeats)
mockSession.$( "isLoggedIn" ).$results( true, false )
expect( mockSession.isLoggedIn() ).toBeTrue() // 1st call
expect( mockSession.isLoggedIn() ).toBeFalse() // 2nd call
expect( mockSession.isLoggedIn() ).toBeTrue() // 3rd call (cycles)$args()Return different values based on what arguments were passed:
mockCache.$( "get" )
.$args( "user-1" ).$results( { id: 1, name: "Luis" } )
.$args( "user-2" ).$results( { id: 2, name: "Alexia" } )
.$args( "missing" ).$results( javaCast( "null", "" ) )$property()// Mock a property on the object
mockService.$property( "timeout", "variables", 5000 )
mockService.$property( "cache", "instance", mockCache )$querySim()var mockQuery = mockDAO.$querySim(
"id, name, email
1 | Luis Majano | [email protected]
2 | Alexia Majano | [email protected]"
)
mockDAO.$( "getUsers", mockQuery )Check how and how many times mocked methods were called:
// Get call count
mockRepo.$count( "findById" ) // calls to findById specifically
mockRepo.$count() // total calls to all mocked methods
// Assert call counts
expect( mockRepo.$times( 1, "findById" ) ).toBeTrue() // called exactly once
expect( mockRepo.$once( "save" ) ).toBeTrue() // alias for $times(1)
expect( mockRepo.$never( "delete" ) ).toBeTrue() // never called
expect( mockRepo.$atLeast( 2, "findById" ) ).toBeTrue()
expect( mockRepo.$atMost( 3, "findById" ) ).toBeTrue()
// Inspect call log (arguments per call)
var log = mockRepo.$callLog()
expect( log.findById[ 1 ].id ).toBe( 42 ) // 1st call, named arg "id"
// Reset all mock state
mockRepo.$reset()class extends="testbox.system.BaseSpec" {
function beforeAll() {
// Create service under test with mocked dependencies
variables.mockUserDAO = createEmptyMock( "repositories.UserDAO" )
variables.mockEmailSvc = createEmptyMock( "services.EmailService" )
variables.service = createMock( "services.UserService" )
variables.service.init( variables.mockUserDAO, variables.mockEmailSvc )
}
function run() {
describe( "UserService.register()", () => {
beforeEach( () => {
variables.mockUserDAO.$reset()
variables.mockEmailSvc.$reset()
} )
it( "creates user and sends welcome email", () => {
var newUser = { id: 99, email: "[email protected]", name: "Test" }
variables.mockUserDAO.$( "create", newUser )
variables.mockEmailSvc.$( "sendWelcome" )
var result = variables.service.register( "[email protected]", "Test" )
expect( result ).toBe( newUser )
expect( variables.mockUserDAO.$once( "create" ) ).toBeTrue()
expect( variables.mockEmailSvc.$once( "sendWelcome" ) ).toBeTrue()
} )
it( "throws when email already exists", () => {
variables.mockUserDAO.$( "existsByEmail", true )
expect( () => variables.service.register( "[email protected]", "Test" ) )
.toThrow( type="DuplicateEmailException" )
} )
} )
}
}mockData())Available in all bundles via the cbMockData module included with TestBox:
// Generate 5 fake user records
var users = mockData(
$num = 5,
id = "autoincrement",
name = "name",
email = "email",
age = "age",
isActive = "boolean",
createdDate = "datetime",
bio = "lorem:2" // 2 paragraphs of lorem ipsum
)
// Random count
var items = mockData( $num = "rnd:5:20", title = "name" )
// Return as struct instead of array
var single = mockData(
$returnType = "struct",
$num = 1,
id = "uuid",
name = "name",
email = "email"
)| Type | Description |
|---|---|
autoincrement | Sequential integer starting from 1 |
uuid | Random UUID |
name | Full name |
fname / lname | First / last name |
email | Email address |
age | Adult age (18–75) |
all_age | Any age (1–100) |
boolean | true or false |
boolean-digit | 0 or 1 |
date / datetime | Random date/datetime |
datetime-iso | ISO 8601 datetime |
num / num:X / num:X:Y | Random numbers |
lorem:N | N paragraphs of lorem ipsum |
baconlorem:N | N paragraphs of bacon lorem |
ipaddress | IPv4 address |
imageurl / imageurl_https | Image URL |
website | Website URL |
phone | Phone number |
oneof:a:b:c | One of the specified values |
Run all specs in a suite concurrently using asyncAll=true:
describe( title="Parallel specs", asyncAll=true, body=() => {
beforeEach( () => {
variables.service = new MyService()
} )
it( "spec one", () => {
expect( variables.service.ping() ).toBeTrue()
} )
it( "spec two", () => {
expect( variables.service.version() ).notToBeEmpty()
} )
} )Warning: In async mode, all variables must be properlyvar-scoped or thread-safe. Do NOT use sharedvariablesscope when running async specs.
attach()Every spec can attach files (screenshots, logs, dumps) to the running spec. They are kept for passed and failed specs and reporters list them (JSON report, links in the Simple report, [[ATTACHMENT|path]] lines in JUnit <system-out>, text/console/stream output under failed specs).
it( "exports the report", () => {
var file = service.export()
attach( file, "file", "export.csv" ) // attach( path, type = "file", name = file name of path )
attach( logPath, "log" )
expect( fileExists( file ) ).toBeTrue()
} )Call it from a spec body or a beforeEach() / afterEach() / aroundEach() closure; outside a running spec it throws TestBox.InvalidContext.
A failing or erroring spec can rerun up to N more times. Each attempt reruns beforeEach(), the body and afterEach() (xUnit: setup(), the test, teardown()), and only the final attempt is recorded. Output shows "(passed after N attempts)".
// 1. Spec argument (highest precedence), also on fit() and xit()
it( title = "talks to a flaky service", retries = 2, body = () => { ... } )
// 2. Bundle annotation (BoxLang annotations go above the declaration)
@retries( 1 )
class extends="testbox.system.BaseSpec" { ... }
// xUnit: method annotation overrides the bundle annotation
@retries( 3 )
function testFlaky() { ... }
// CFML components keep inline attributes: component extends="testbox.system.BaseSpec" retries="1" { ... }# 3. Global default (lowest precedence)
./testbox/run --retries=2Skipped specs are never retried. Use retries for genuinely flaky I/O (browsers, networks), not to hide bugs.
results.getFailedTargets() returns what failed in a run, on every engine: { bundles, specs, bundleErrors } (bundle paths, spec ids, and bundles that failed outside of a spec). Run only those with the existing filters:
var failed = results.getFailedTargets()
new testbox.system.TestBox( bundles = failed.bundles ).runRaw( testBundles = failed.bundles, testSpecs = failed.specs )?testBundles=...&testSpecs=...) is built from the report itself, so web runners keep no state; past 2000 characters it reruns the failed bundles whole.--failed: see below.#### --failed
Every BoxLang runner run writes {reportpath}/.testbox-failed.json with the bundles and specs that failed or errored. ./testbox/run --failed reruns only those; when the file is missing or empty it prints a message and runs nothing. A bundle that failed outside of a spec (beforeAll(), afterAll()) is broken, not flaky: it goes to bundleErrors in the file and is listed, never rerun. The other bundles still rerun only their failed specs.
Playwright.AssertionFailed (thrown by bx-playwright assertions such as page.assertSee()) counts as a spec failure, like TestBox.AssertionFailed, keeping its message and detail. Other Playwright.* exceptions count as errors.
For browser tests annotate the spec with @browser (or @browserProfile( "ci" ) / @baseURL( "http://..." )) above class extends="testbox.system.BaseSpec" (in ColdBox apps, coldbox.system.testing.BaseTestCase) and use browse(), the browser matchers (expect( page ).toSee( "Welcome" )) and the --web-server runner option. Load the bx-playwright-testing skill for the full API.
# Run all tests (default: tests/specs)
./testbox/run
# Run a specific bundle (dot notation)
./testbox/run tests.specs.UserServiceSpec
# Run all specs in a directory
./testbox/run --directory=tests.specs.unit
# Specify a bundle pattern
./testbox/run --bundles-pattern=*Spec*.bx
# Stream results in real-time (great for CI)
./testbox/run --stream
# Stop on first failure
./testbox/run --eager-failure
# Verbose output (every spec printed)
./testbox/run --verbose
# Retry failing specs, or rerun only the last failures
./testbox/run --retries=2
./testbox/run --failed
# Start the app before the tests (browser tests)
./testbox/run --web-server="boxlang-miniserver --port 8080" --web-server-url=http://localhost:8080| Option | Default | Description | ||
|---|---|---|---|---|
--bundles | * | Dot-notation bundle list | ||
--bundles-pattern | `Spec.bx\ | Test.bx\ | ...` | Glob pattern |
--directory | tests.specs | Directory to scan | ||
--recurse | true | Recurse into subdirectories | ||
--eager-failure | false | Stop on first failure | ||
--verbose | false | Print each spec as it runs | ||
--stream | false | Real-time streaming output | ||
--retries | 0 | Extra runs for failing or erroring specs (spec and bundle values win) | ||
--failed | false | Run only the bundles and specs that failed or errored in the last run | ||
--web-server | Shell command that starts a web server before the tests, stopped after them | |||
--web-server-url | http://localhost:8080 | URL polled until the server answers; default baseURL of browser-enabled (@browser) bundles | ||
--web-server-timeout | 60 | Seconds to wait for the server, exits with code 1 when it does not answer |
box testbox run
box testbox run bundles=tests.specs.UserServiceSpec
box testbox run --verbosePick one with reporter= in the runner URL, --reporter= on the BoxLang CLI runner, or reporter = { type : "...", options : {} } in code.
| Reporter | Output | |
|---|---|---|
Simple | The complete HTML report: failures first, then every bundle, with Ask AI and editor links | |
Min / Dot / Doc | HTML: only what needs attention / one dot per spec with a details drawer / documentation style | |
Text, MinText, Console | Plain text | |
JSON, Raw, XML | Full results as data | |
JUnit, ANTJunit | JUnit XML for CI (attachments as `[[ATTACHMENT\ | path]] lines in <system-out>`) |
Agent | Compact JSON for AI agents: totals plus only the failures |
F jumps to the next failure, / searches.?editor=vscode (default) and other editors turn failures and stack frames into open-in-editor links.reporter = {
type : "testbox.system.reports.SimpleReporter",
options : {
aiAssist : true, // false (or ?aiAssist=false) removes Ask AI
aiContextLines : 5, // code lines around the failing line
aiStackFrames : 8,
aiProviders : [ { id : "acme", name : "Acme AI", url : "https://ai.acme.test/?p={prompt}" } ],
aiPrompt : "{intro} {spec} ({status}) {message} {code} {stack} Rerun: {rerun}",
urlParams : { editor : "idea" } // request params for reports produced from code (no url scope)
}
}reporter=agent)One minified JSON line: {"ok":false,"totals":{"pass","fail","error","skipped","specs","ms"},"failures":[{"bundle","spec","status","message","at"}],"truncated":0}. Branch on ok; the exit code is not changed. Options: detail (summary, failures, all), maxFailures (20), maxMessageLength (300), includeStack (false), stackDepth (3), includeSkipped, includeDebug. Prefer it when an agent runs the tests: ./testbox/run --reporter=agent.
tests/
├── specs/
│ ├── unit/
│ │ ├── UserServiceSpec.bx
│ │ └── OrderServiceSpec.bx
│ ├── integration/
│ │ └── UserFlowSpec.bx
│ └── resources/
│ └── fixtures.json
├── Application.bx ← configures mappings for tests
└── runner.bxm ← web runner templateApplication.bx for Testsclass {
this.name = "MyAppTests"
this.mappings[ "/testbox" ] = expandPath( "/testbox" )
this.mappings[ "/app" ] = expandPath( "../src" )
}function beforeAll() {
variables.mockRepo = createEmptyMock( "UserRepository" )
variables.mockMailer = createEmptyMock( "Mailer" )
variables.service = new UserService( variables.mockRepo, variables.mockMailer )
}prepareMockit( "validates email format internally", () => {
var svc = prepareMock( new UserService() )
// expose private method for targeted testing
expect( svc.validateEmail( "bad-email" ) ).toBeFalse()
expect( svc.validateEmail( "[email protected]" ) ).toBeTrue()
} )var validInputs = [ "hello", "world", "boxlang" ]
for ( var word in validInputs ) {
it(
title = "validates '#word#' as a valid slug",
data = { word = word },
body = ( data ) => {
expect( SlugValidator.isValid( data.word ) ).toBeTrue()
}
)
}it( "returns paginated user query", () => {
var mockQ = mockDAO.$querySim(
"id, name
1 | Luis
2 | Alexia"
)
mockDAO.$( "paginate", mockQ )
var result = variables.service.getPage( 1, 10 )
expect( result ).toBeQuery()
expect( result.recordCount ).toBe( 2 )
} )Always reset mocks in beforeEach to avoid state bleed:
beforeEach( () => {
variables.mockRepo.$reset()
variables.mockMailer.$reset()
} )| Anti-Pattern | Better Approach |
|---|---|
| Testing implementation details (private internals) | Test public behaviour; use prepareMock sparingly |
| Shared mutable state between specs | Reset in beforeEach, use afterEach to clean up |
fit() / fdescribe() committed to version control | Use focused specs only locally; CI must run all |
| One assertion per spec when they are naturally grouped | Group related assertions in one spec; split logically unrelated specs |
| Not resetting mock call logs between specs | Call $reset() in beforeEach |
Using asyncAll=true with shared variables scope | Ensure all state is var-scoped in async suites |
| Catch blocks swallowing exceptions in tests | Let exceptions propagate; rely on toThrow() |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.