コンテンツにスキップ

テスト

VaporTesting

VaporにはVaporTestingというモジュールが含まれており、Swift Testingをベースとしたテストヘルパーを提供しています。これらのテストヘルパーを使用すると、Vaporアプリケーションにプログラムでテストリクエストを送信したり、HTTPサーバー経由で実行したりできます。

Note

新しいプロジェクトやSwift並行処理を採用しているチームには、XCTestよりもSwift Testingを強く推奨します。

はじめに

VaporTestingモジュールを使用するには、パッケージのテストターゲットに追加されていることを確認してください。

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.110.1")
    ],
    targets: [
        ...
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "VaporTesting", package: "vapor"),
        ])
    ]
)

Warning

対応するテストモジュールを使用することを確認してください。そうしないと、Vaporのテスト失敗が適切に報告されない可能性があります。

次に、テストファイルの先頭にimport VaporTestingimport Testingを追加します。テストケースを記述するために@Suite名を持つ構造体を作成します。

@testable import App
import VaporTesting
import Testing

@Suite("App Tests")
struct AppTests {
    @Test("Test Stub")
    func stub() async throws {
        // ここでテストします。
    }
}

@Testでマークされた各関数は、アプリがテストされるときに自動的に実行されます。

テストがシリアル化された方法で実行されることを確実にするには(例:データベースでテストする場合)、テストスイート宣言に.serializedオプションを含めます:

@Suite("App Tests with DB", .serialized)

テスト可能なアプリケーション

テストのセットアップとティアダウンを効率化し標準化するために、プライベートメソッド関数withAppを定義します。このメソッドはApplicationインスタンスのライフサイクル管理をカプセル化し、各テストでアプリケーションが適切に初期化、設定、シャットダウンされることを保証します。

特に、起動時にアプリケーションが要求するスレッドを解放することが重要です。各単体テスト後にアプリでasyncShutdown()を呼び出さない場合、Applicationの新しいインスタンスのスレッドを割り当てる際に、precondition失敗でテストスイートがクラッシュする可能性があります。

private func withApp(_ test: (Application) async throws -> ()) async throws {
    let app = try await Application.make(.testing)
    do {
        try await configure(app)
        try await test(app)
    }
    catch {
        try await app.asyncShutdown()
        throw error
    }
    try await app.asyncShutdown()
}

設定を適用するために、Applicationをパッケージのconfigure(_:)メソッドに渡します。その後、test()メソッドを呼び出してアプリケーションをテストします。テスト専用の設定も適用できます。

リクエストの送信

アプリケーションにテストリクエストを送信するには、withAppプライベートメソッドを使用し、その中でapp.testing().test()メソッドを使用します:

@Test("Test Hello World Route")
func helloWorld() async throws {
    try await withApp { app in
        try await app.testing().test(.GET, "hello") { res async in
            #expect(res.status == .ok)
            #expect(res.body.string == "Hello, world!")
        }
    }
}

最初の2つのパラメータは、HTTPメソッドとリクエストするURLです。末尾のクロージャは、#expectマクロを使用して検証できるHTTPレスポンスを受け取ります。

より複雑なリクエストの場合、beforeRequestクロージャを提供してヘッダーを変更したり、コンテンツをエンコードしたりできます。VaporのContent APIは、テストリクエストとレスポンスの両方で利用できます。

let newDTO = TodoDTO(id: nil, title: "test")

try await app.testing().test(.POST, "todos", beforeRequest: { req in
    try req.content.encode(newDTO)
}, afterResponse: { res async throws in
    #expect(res.status == .ok)
    let models = try await Todo.query(on: app.db).all()
    #expect(models.map({ $0.toDTO().title }) == [newDTO.title])
})

テストメソッド

Vaporのテスト用APIは、プログラムでのテストリクエスト送信と、ライブHTTPサーバー経由での送信の両方をサポートしています。testingメソッドを通じて使用したい方法を指定できます。

// プログラムによるテストを使用。
app.testing(method: .inMemory).test(...)

// ライブHTTPサーバー経由でテストを実行。
app.testing(method: .running).test(...)

デフォルトではinMemoryオプションが使用されます。

runningオプションは、使用する特定のポートを渡すことをサポートしています。デフォルトでは8080が使用されます。

app.testing(method: .running(port: 8123)).test(...)

データベース統合テスト

テスト中にライブデータベースが使用されないように、テスト専用にデータベースを設定します。

app.databases.use(.sqlite(.memory), as: .sqlite)

その後、autoMigrate()autoRevert()を使用してテスト中のデータベーススキーマとデータライフサイクルを管理することで、テストを強化できます:

これらのメソッドを組み合わせることで、各テストが新しく一貫したデータベース状態で開始されることを保証し、テストをより信頼性の高いものにし、残存データによる偽陽性や偽陰性の可能性を減らすことができます。

更新された設定を含むwithApp関数は次のようになります:

private func withApp(_ test: (Application) async throws -> ()) async throws {
    let app = try await Application.make(.testing)
    app.databases.use(.sqlite(.memory), as: .sqlite)
    do {
        try await configure(app)
        try await app.autoMigrate()
        try await test(app)
        try await app.autoRevert()   
    }
    catch {
        try? await app.autoRevert()
        try await app.asyncShutdown()
        throw error
    }
    try await app.asyncShutdown()
}

XCTVapor

VaporにはXCTVaporというモジュールが含まれており、XCTestをベースとしたテストヘルパーを提供しています。これらのテストヘルパーを使用すると、Vaporアプリケーションにプログラムでテストリクエストを送信したり、HTTPサーバー経由で実行したりできます。

はじめに

XCTVaporモジュールを使用するには、パッケージのテストターゲットに追加されていることを確認してください。

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
    ],
    targets: [
        ...
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

次に、テストファイルの先頭にimport XCTVaporを追加します。テストケースを記述するためにXCTestCaseを拡張するクラスを作成します。

import XCTVapor

final class MyTests: XCTestCase {
    func testStub() throws {
        // ここでテストします。
    }
}

testで始まる各関数は、アプリがテストされるときに自動的に実行されます。

テスト可能なアプリケーション

.testing環境を使用してApplicationのインスタンスを初期化します。このアプリケーションがdeinitializeされる前にapp.shutdown()を呼び出す必要があります。

シャットダウンは、アプリが要求したリソースの解放を助けるために必要です。特に、起動時にアプリケーションが要求するスレッドを解放することが重要です。各単体テスト後にアプリでshutdown()を呼び出さない場合、Applicationの新しいインスタンスのスレッドを割り当てる際に、precondition失敗でテストスイートがクラッシュする可能性があります。

let app = Application(.testing)
defer { app.shutdown() }
try configure(app)

設定を適用するために、Applicationをパッケージのconfigure(_:)メソッドに渡します。テスト専用の設定は後で適用できます。

リクエストの送信

アプリケーションにテストリクエストを送信するには、testメソッドを使用します。

try app.test(.GET, "hello") { res in
    XCTAssertEqual(res.status, .ok)
    XCTAssertEqual(res.body.string, "Hello, world!")
}

最初の2つのパラメータは、HTTPメソッドとリクエストするURLです。末尾のクロージャは、XCTAssertメソッドを使用して検証できるHTTPレスポンスを受け取ります。

より複雑なリクエストの場合、beforeRequestクロージャを提供してヘッダーを変更したり、コンテンツをエンコードしたりできます。VaporのContent APIは、テストリクエストとレスポンスの両方で利用できます。

try app.test(.POST, "todos", beforeRequest: { req in
    try req.content.encode(["title": "Test"])
}, afterResponse: { res in
    XCTAssertEqual(res.status, .created)
    let todo = try res.content.decode(Todo.self)
    XCTAssertEqual(todo.title, "Test")
})

テスト可能なメソッド

Vaporのテスト用APIは、プログラムでのテストリクエスト送信と、ライブHTTPサーバー経由での送信の両方をサポートしています。testableメソッドを使用して、使用したい方法を指定できます。

// プログラムによるテストを使用。
app.testable(method: .inMemory).test(...)

// ライブHTTPサーバー経由でテストを実行。
app.testable(method: .running).test(...)

デフォルトではinMemoryオプションが使用されます。

runningオプションは、使用する特定のポートを渡すことをサポートしています。デフォルトでは8080が使用されます。

.running(port: 8123)