mcpField
B
boxlang-testing

boxlang-testing

github.com/ortus-boxlangテスト・品質049 回インストール1 週間前に更新
リポジトリ

boxlang-testing について

BoxLang 標準のテストフレームワーク TestBox を使うテストスキルです。開発者や AI エージェントが BDD または xUnit 形式のテストを書き、アサーションや MockBox を利用できます。

主な機能

  1. 01BDD と xUnit のテスト形式
  2. 02チェーン可能な期待値とアサーション
  3. 03MockBox によるモックと呼び出し検証
  4. 04モックデータの生成
  5. 05スペックの非同期実行
  6. 06スペックのスキップ、フォーカス、再試行

活用シーン

  1. 01BoxLang アプリケーションの動作をテスト
  2. 02サービスやリポジトリの依存先をモック
  3. 03テスト用フィクスチャデータを生成
  4. 04テストスペックを並行実行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ortus-boxlang/skills --skill boxlang-testing

インストール後、Claude Code で/boxlang-testingを入力して読み込まれたか確認します。

詳細

BoxLang と TestBox によるテスト

BoxLang は、開発者と AI エージェントが協力してアプリケーションを構築、モダナイズ、実行するための、AI ネイティブなソフトウェア生産性プラットフォームです。

概要

TestBox は BoxLang の標準テストフレームワークです。次の 2 つのスタイルに対応しています。

  • 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 によるイントロスペクション、ウェブランナーからの直接実行、テスト読み込みの高速化が可能になります。

インジェクションされる変数(バンドルで常に使用可能)
変数用途
$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()
                } )
            } )
        } )
    } )
} )
Spec 関数
// 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)

ライフサイクルメソッドは、spec の前後に実行するセットアップと後処理を制御します。

グローバル(バンドル全体)
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}()

スキップとフォーカス

Spec/スイートのスキップ

スイート/spec 関数の先頭に 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
} )
Spec/スイートへのフォーカス

先頭に f を付けるか、focused=true を使用します。フォーカスした spec/スイートのみが実行されます。

// 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 ではそれらの spec のみが実行されます。


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"
)
利用可能なモックの種類
種類説明
autoincrement1から始まる連続した整数
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:Nlorem ipsumのN段落
baconlorem:Nbacon loremのN段落
ipaddressIPv4アドレス
imageurl / imageurl_https画像URL
websiteWebサイトURL
phone電話番号
oneof:a:b:c指定された値のいずれか1つ

非同期テスト

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 }(バンドルのパス、テストID、テスト外で失敗したバンドル)。既存のフィルターを使用して、それらのみを実行します。

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文字を超えると、失敗したバンドル全体を再実行します。
  • BoxLang CLIの--failed:以下を参照してください。
--failed

BoxLangの各ランナー実行では、失敗またはエラーになったバンドルとテストを含む{reportpath}/.testbox-failed.jsonが書き込まれます。./testbox/run --failedはそれらのみを再実行します。ファイルが存在しないか空の場合は、メッセージを表示して何も実行しません。テスト外(beforeAll()、afterAll())で失敗したバンドルは、不安定なのではなく壊れているため、ファイル内のbundleErrorsに追加され、一覧表示されますが、再実行されません。それ以外のバンドルでは、失敗したテストのみが再実行されます。

Playwrightの失敗

bx-playwrightのアサーション(page.assertSee()など)がスローするPlaywright.AssertionFailedは、TestBox.AssertionFailedと同様にテストの失敗として扱われ、そのメッセージと詳細が保持されます。それ以外のPlaywright.*例外はエラーとして扱われます。

ブラウザーテスト

ブラウザーテストでは、class extends="testbox.system.BaseSpec"(ColdBoxアプリではcoldbox.system.testing.BaseTestCase)の上に@browser(または@browserProfile( "ci" )/@baseURL( "http://..." ))を付与し、browse()、ブラウザーマッチャー(expect( page ).toSee( "Welcome" ))、およびランナーオプションの--web-serverを使用します。APIの全容については、bx-playwright-testingスキルを読み込んでください。


テストの実行

BoxLang CLIランナー
# 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*ドット記法のバンドル一覧
--bundles-pattern*Spec*.bx|*Test*.bx|...グロブパターン
--directorytests.specsスキャンするディレクトリ
--recursetrueサブディレクトリも再帰的に処理します
--eager-failurefalse最初の失敗で停止します
--verbosefalse各スペックの実行時に出力します
--streamfalseリアルタイムで出力をストリーミングします
--retries0失敗またはエラーになったスペックの追加実行回数(スペックとバンドルの値が優先されます)
--failedfalse前回の実行で失敗またはエラーになったバンドルとスペックのみを実行します
--web-serverテスト前にウェブサーバーを起動し、テスト後に停止するシェルコマンド
--web-server-urlhttp://localhost:8080サーバーから応答があるまでポーリングする URL。ブラウザー対応(@browser)バンドルのデフォルト 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 レポート。失敗を先頭に表示し、すべてのバンドルと Ask AI およびエディターへのリンクを含みます
Min / Dot / DocHTML:要対応項目のみ/スペックごとにドット 1 つと詳細ドロワー/ドキュメント形式
Text、MinText、Consoleプレーンテキスト
JSON、Raw、XML全結果をデータとして出力
JUnit、ANTJunitCI 用の JUnit XML(アタッチメントは <system-out> 内の [[ATTACHMENT|path]] 行として出力)
AgentAI エージェント向けのコンパクトな JSON:合計数と失敗のみ
HTML レポーター(Simple、Min、Dot、Doc)
  • 最初に判定バナーを表示します(件数、割合バー、ステータスフィルター)。ライト、ダーク、システムのテーマに対応し、F で次の失敗に移動、/ で検索できます。
  • すべてのバンドル、スイート、スペックに 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)

1 行の最小化された 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。エージェントがテストを実行する場合は、こちらを推奨します:./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()
} )
データ駆動型スペック
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 )
} )
テスト間でのモックのリセット

状態の引き継ぎを防ぐため、beforeEach でモックを必ずリセットします:

beforeEach( () => {
    variables.mockRepo.$reset()
    variables.mockMailer.$reset()
} )

避けるべきアンチパターン

アンチパターンより良いアプローチ
実装の詳細(非公開の内部処理)をテストする公開された動作をテストし、prepareMock は必要最小限に使用します
スペック間で変更可能な状態を共有するbeforeEach でリセットし、afterEach でクリーンアップします
fit() / fdescribe() をバージョン管理にコミットするフォーカスしたスペックはローカルでのみ使用し、CI ではすべて実行します
自然にまとめられるのに、スペックごとにアサーションを 1 つだけにする関連するアサーションは 1 つのスペックにまとめ、論理的に無関係なスペックは分割します
スペック間でモックの呼び出しログをリセットしないbeforeEach で $reset() を呼び出します
共有された variables スコープで asyncAll=true を使用する非同期スイートでは、すべての状態が var スコープになるようにします
テストの catch ブロックで例外を握りつぶす例外を伝播させ、toThrow() による検証に任せます

コメント