关于 boxlang-testing
此技能介绍 BoxLang 标准测试框架 TestBox,面向开发者和 AI Agent。它支持 BDD 与 xUnit 测试,并提供断言、MockBox 模拟及测试数据生成功能。
主要功能
- 01支持 BDD 和 xUnit 测试风格
- 02提供链式期望与断言
- 03使用 MockBox 模拟并验证调用
- 04生成模拟数据
- 05异步运行测试规格
- 06跳过、聚焦和重试测试规格
适用场景
- 01测试 BoxLang 应用行为
- 02模拟服务和仓库依赖
- 03为测试生成夹具数据
- 04并发运行测试规格
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ortus-boxlang/ skills --skill boxlang-testing安装后在 Claude Code 中输入/boxlang-testing确认已加载。
详细介绍
使用 TestBox 测试 BoxLang
BoxLang 是一个以 AI 为原生核心的软件生产力平台,可用于构建、现代化改造和运行应用,并让开发者与 AI Agent 协同工作。
概览
TestBox 是 BoxLang 的标准测试框架。它支持两种风格:
- BDD(行为驱动开发):
describe()、it()、feature()、story()、given()/when()/then() - xUnit:基于类、
test*()方法、setup()/tearDown()
两种风格使用相同的断言/预期库和用于模拟的 MockBox。
测试包
测试包是一个包含测试的 BoxLang 类(.bx 文件)。按照惯例,文件名以
Test 或 Spec 结尾: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" )
} )
} )
}
}
继承 testbox.system.BaseSpec 并非必需,但建议这样做:它可启用 IDE
内省、直接通过 Web 运行器执行,以及更快地加载测试。
注入的变量(测试包中始终可用)
| 变量 | 用途 |
|---|---|
$mockbox | 用于创建模拟对象的 MockBox 实例 |
$assert | 断言库 |
$utility | 实用工具 |
$customMatchers | 自定义匹配器注册表 |
$testID | 测试包的唯一 ID |
$debugBuffer | 调试输出缓冲区 |
BDD 风格
基本结构
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()
} )
} )
}
}
套件函数(作用相同的代码块)
| 函数 | 别名/用途 |
|---|---|
describe( title, body ) | 标准套件 |
feature( title, body ) | 功能级套件 |
story( title, body ) | 故事级套件 |
scenario( title, body ) | 功能/故事中的场景 |
given( title, body ) | Given 代码块(GWT) |
when( title, body ) | When 代码块(GWT) |
then( title, body ) | Then 代码块/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()
} )
生命周期方法(BDD)
生命周期方法用于控制规格测试前后的设置和清理。
全局(整个测试包)
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() { ... }
}
套件级(BDD: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" )
} )
} )
beforeEach沿嵌套树向下执行(外层 → 内层)。afterEach沿树向上执行(内层 → 外层)。
生命周期注解(用于父类)
为任意函数添加注解,即可挂接到生命周期中,而无需重写命名方法。 适用于基础测试类:
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"
}
}
}
}
生命周期数据绑定
向生命周期方法传递数据,以便在循环中正确捕获动态值:
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 )
}
)
} )
}
xUnit 风格
@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" )
}
}
预期(流式 API)
使用 expect( actual ) 开始流式断言链。为任意匹配器添加前缀
not,即可使用其否定版本。
常用匹配器
// 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)
一次测试数组或结构体中的每个元素:
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)
传统断言库,可通过每个测试包中的 $assert 使用:
$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}()
跳过和聚焦
跳过规格/套件
在任意套件/规格函数前加上 x,或使用 skip 参数:
// 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
} )
聚焦规格/套件
在前面加上 f,或使用 focused=true:只运行已聚焦的规格/套件:
// 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", () => { ... } )
} )
警告:切勿提交已聚焦的测试:CI 只会运行这些规格。
使用 MockBox 进行模拟
MockBox 内置于 TestBox 中。每个测试包都可以使用辅助方法。
创建模拟对象
// 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()
按顺序返回值,耗尽后循环使用:
// 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()
根据传入的参数返回不同的值:
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 )
验证方法
检查模拟方法的调用方式和次数:
// 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())
TestBox 附带的 cbMockData 模块可让所有测试包使用此功能:
// 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"
)
可用的模拟类型
| 类型 | 描述 |
|---|---|
autoincrement | 从 1 开始递增的整数 |
uuid | 随机 UUID |
name | 全名 |
fname / lname | 名 / 姓 |
email | 电子邮件地址 |
age | 成年人年龄(18–75) |
all_age | 任意年龄(1–100) |
boolean | true 或 false |
boolean-digit | 0 或 1 |
date / datetime | 随机日期/日期时间 |
datetime-iso | ISO 8601 日期时间 |
num / num:X / num:X:Y | 随机数字 |
lorem:N | N 段 lorem ipsum |
baconlorem:N | N 段 bacon lorem |
ipaddress | IPv4 地址 |
imageurl / imageurl_https | 图片 URL |
website | 网站 URL |
phone | 电话号码 |
oneof:a:b:c | 指定值之一 |
异步测试
使用 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()
} )
} )
警告:在异步模式下,所有变量都必须使用
var正确限定作用域,或确保线程安全。异步运行规范时,切勿使用共享的variables作用域。
附件、重试和浏览器测试
为规范附加文件:attach()
每个规范都可以为正在运行的规范附加文件(屏幕截图、日志、转储)。无论规范通过还是失败,附件都会保留,报告器也会列出它们(JSON 报告、Simple 报告中的链接、JUnit <system-out> 中的 [[ATTACHMENT|path]] 行,以及失败规范下的文本/控制台/流输出)。
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()
} )
可在规范主体或 beforeEach() / afterEach() / aroundEach() 闭包中调用;在没有正在运行的规范时调用会抛出 TestBox.InvalidContext。
重试
失败或出错的规范最多可以额外重跑 N 次。每次尝试都会重新运行 beforeEach()、主体和 afterEach()(xUnit:setup()、测试、teardown()),且只记录最后一次尝试。输出会显示“(在 N 次尝试后通过)”。
// 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=2
跳过的规范不会重试。重试适用于确实不稳定的 I/O(浏览器、网络),而非掩盖错误。
仅重跑失败项
results.getFailedTargets() 会返回本次运行中失败的内容,适用于所有引擎:{ bundles, specs, bundleErrors }(bundle 路径、规范 ID,以及在规范之外失败的 bundle)。使用现有筛选条件仅运行这些内容:
var failed = results.getFailedTargets()
new testbox.system.TestBox( bundles = failed.bundles ).runRaw( testBundles = failed.bundles, testSpecs = failed.specs )
- HTML 报告(Simple、Min、Dot、Doc):Run All 旁边会显示 Run Failed (N) 按钮,且仅在有失败项时显示。其链接(
?testBundles=...&testSpecs=...)由报告本身生成,因此 Web 运行器不会保存状态;超过 2000 个字符时,会重新运行整个失败的 bundle。 - BoxLang 命令行
--failed:见下文。
--failed
每次 BoxLang 运行器运行都会在 {reportpath}/.testbox-failed.json 中写入失败或出错的 bundles 和规范。./testbox/run --failed 仅重跑这些内容;如果文件不存在或为空,它会输出提示且不运行任何内容。在规范之外失败的 bundle(beforeAll()、afterAll())属于故障,而非不稳定项:它会写入文件中的 bundleErrors 并列出,绝不会重跑。其他 bundles 仍然只重跑失败的规范。
Playwright 失败
Playwright.AssertionFailed(由 bx-playwright 断言抛出,例如 page.assertSee())会被视为规范失败,与 TestBox.AssertionFailed 一样,并保留其消息和详情。其他 Playwright.* 异常会被视为错误。
浏览器测试
对于浏览器测试,请在 class extends="testbox.system.BaseSpec" 上方使用 @browser(或 @browserProfile( "ci" ) / @baseURL( "http://..." ))注解规范(在 ColdBox 应用中使用 coldbox.system.testing.BaseTestCase),并使用 browse()、浏览器匹配器(expect( page ).toSee( "Welcome" ))以及 --web-server 运行器选项。加载 bx-playwright-testing 技能以查看完整 API。
运行测试
BoxLang 命令行运行器
# 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
CLI 运行器选项
| 选项 | 默认值 | 描述 |
|---|---|---|
--bundles | * | 点号表示法的 bundle 列表 |
--bundles-pattern | *Spec*.bx|*Test*.bx|... | Glob 模式 |
--directory | tests.specs | 要扫描的目录 |
--recurse | true | 递归进入子目录 |
--eager-failure | false | 首次失败时停止 |
--verbose | false | 每个 spec 运行时都打印出来 |
--stream | false | 实时流式输出 |
--retries | 0 | 对失败或出错的 spec 额外运行的次数(spec 和 bundle 的值优先) |
--failed | false | 仅运行上次运行中失败或出错的 bundle 和 spec |
--web-server | 在测试前启动 Web 服务的 Shell 命令,测试结束后停止 | |
--web-server-url | http://localhost:8080 | 轮询此 URL,直到服务响应;作为启用了浏览器的(@browser)bundle 的默认 baseURL |
--web-server-timeout | 60 | 等待服务的秒数;若服务无响应,则以代码 1 退出 |
CommandBox 运行器
box testbox run
box testbox run bundles=tests.specs.UserServiceSpec
box testbox run --verbose
报告器
在运行器 URL 中使用 reporter=、在 BoxLang CLI 运行器中使用 --reporter=,或在代码中使用 reporter = { type : "...", options : {} },即可选择报告器。
| 报告器 | 输出 |
|---|---|
Simple | 完整的 HTML 报告:先显示失败项,然后显示每个 bundle,并附带 Ask AI 和编辑器链接 |
Min / Dot / Doc | HTML:仅显示需要关注的内容 / 每个 spec 显示一个圆点并带有详情抽屉 / 文档样式 |
Text、MinText、Console | 纯文本 |
JSON、Raw、XML | 将完整结果作为数据输出 |
JUnit、ANTJunit | 用于 CI 的 JUnit XML(附件以 [[ATTACHMENT|path]] 行的形式包含在 <system-out> 中) |
Agent | 面向 AI Agent 的紧凑 JSON:包含总计数据和仅失败项 |
HTML 报告器(Simple、Min、Dot、Doc)
- 首先显示结论横幅(计数、比例条、状态筛选器),支持浅色、深色和系统主题;按
F跳转到下一个失败项,按/搜索。 - 每个 bundle、suite 和 spec 都有 Run 链接;还提供 Run All,以及有失败项时显示的 Run Failed (N)(从报告生成,不保存状态,见“仅重新运行失败项”)。
?editor=vscode(默认值)及其他编辑器会将失败项和堆栈帧转换为“在编辑器中打开”链接。- 每个失败项都提供 Ask AI:可复制或预览提示词,在 ChatGPT 或 Claude 中打开,或将其复制为 Agent JSON。点击提供方之前,任何内容都不会离开页面。
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)
}
}
Agent 报告器(reporter=agent)
输出一行最小化 JSON:{"ok":false,"totals":{"pass","fail","error","skipped","specs","ms"},"failures":[{"bundle","spec","status","message","at"}],"truncated":0}。根据 ok 分支处理;退出代码不会改变。选项:detail(summary、failures、all)、maxFailures(20)、maxMessageLength(300)、includeStack(false)、stackDepth(3)、includeSkipped、includeDebug。Agent 运行测试时优先使用此报告器:./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 template
用于测试的最简 Application.bx
class {
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 )
}
通过 prepareMock 测试私有方法
it( "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()
} )
数据驱动的 Specs
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 )
} )
在测试之间重置 Mock
务必在 beforeEach 中重置 Mock,以避免状态泄漏:
beforeEach( () => {
variables.mockRepo.$reset()
variables.mockMailer.$reset()
} )
应避免的反模式
| 反模式 | 更好的做法 |
|---|---|
| 测试实现细节(私有内部逻辑) | 测试公开行为;谨慎使用 prepareMock |
| 在测试用例之间共享可变状态 | 在 beforeEach 中重置,并使用 afterEach 清理 |
将 fit() / fdescribe() 提交到版本控制中 | 仅在本地运行聚焦测试;CI 必须运行全部测试 |
| 将本来自然归为一组的断言拆成每个测试用例一个 | 将相关断言归为一个测试用例;将逻辑上无关的测试拆开 |
| 未在测试用例之间重置模拟调用日志 | 在 beforeEach 中调用 $reset() |
在共享 variables 作用域中使用 asyncAll=true | 确保异步测试套件中的所有状态都限定在 var 作用域内 |
在测试中的 catch 块里吞掉异常 | 让异常继续传播;依靠 toThrow() |