mcpField
B
boxlang-testing

boxlang-testing

github.com/ortus-boxlang测试与质量049 次安装1 周前更新
代码仓库

关于 boxlang-testing

此技能介绍 BoxLang 标准测试框架 TestBox,面向开发者和 AI Agent。它支持 BDD 与 xUnit 测试,并提供断言、MockBox 模拟及测试数据生成功能。

主要功能

  1. 01支持 BDD 和 xUnit 测试风格
  2. 02提供链式期望与断言
  3. 03使用 MockBox 模拟并验证调用
  4. 04生成模拟数据
  5. 05异步运行测试规格
  6. 06跳过、聚焦和重试测试规格

适用场景

  1. 01测试 BoxLang 应用行为
  2. 02模拟服务和仓库依赖
  3. 03为测试生成夹具数据
  4. 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)
booleantrue 或 false
boolean-digit0 或 1
date / datetime随机日期/日期时间
datetime-isoISO 8601 日期时间
num / num:X / num:X:Y随机数字
lorem:NN 段 lorem ipsum
baconlorem:NN 段 bacon lorem
ipaddressIPv4 地址
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 模式
--directorytests.specs要扫描的目录
--recursetrue递归进入子目录
--eager-failurefalse首次失败时停止
--verbosefalse每个 spec 运行时都打印出来
--streamfalse实时流式输出
--retries0对失败或出错的 spec 额外运行的次数(spec 和 bundle 的值优先)
--failedfalse仅运行上次运行中失败或出错的 bundle 和 spec
--web-server在测试前启动 Web 服务的 Shell 命令,测试结束后停止
--web-server-urlhttp://localhost:8080轮询此 URL,直到服务响应;作为启用了浏览器的(@browser)bundle 的默认 baseURL
--web-server-timeout60等待服务的秒数;若服务无响应,则以代码 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 / DocHTML:仅显示需要关注的内容 / 每个 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()

评论

支持 Markdown