Skip to content
58 changes: 35 additions & 23 deletions docs/advanced/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,26 @@ To ensure your tests run in a serialized manner (e.g., when testing with a datab

### Testable Application

Define a private method function `withApp` to streamline and standardize the setup and teardown for our tests. This method encapsulates the lifecycle management of the `Application` instance, ensuring that the application is properly initialized, configured, and shut down for each test.
To provide a streamlined and standardized setup and teardown of tests, `VaporTesting` offers the `withApp` helper function. This method encapsulates the lifecycle management of the `Application` instance, ensuring that the application is properly initialized, configured, and shut down for each test.

In particular it is important to release the threads the application requests at startup. If you do not call `asyncShutdown()` on the app after each unit test, you may find your test suite crash with a precondition failure when allocating threads for a new instance of `Application`.
Pass your application's `configure(_:)` method to the `withApp` helper function to make sure all your routes get correctly registered:

```swift
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
@Test func someTest() async throws {
try await withApp(configure: configure) { app in
// your actual test
}
try await app.asyncShutdown()
}
```

Pass the `Application` to your package's `configure(_:)` method to apply your configuration. Then you test the application calling the `test()` method. Any test-only configurations can also be applied.

#### Send Request

To send a test request to your application, use the `withApp` private method and inside use the `app.testing().test()` method:

```swift
@Test("Test Hello World Route")
func helloWorld() async throws {
try await withApp { app in
try await withApp(configure: configure) { app in
try await app.testing().test(.GET, "hello") { res async in
#expect(res.status == .ok)
#expect(res.body.string == "Hello, world!")
Expand Down Expand Up @@ -131,22 +122,28 @@ app.testing(method: .running(port: 8123)).test(...)

#### Database Integration Tests

Configure the database specifically for testing to ensure that your live database is never used during tests.
Configure the database specifically for testing to ensure that your live database is never used during tests. For example, when you are using SQLite, you could configure your database in the `configure(_:)` function as follows:

```swift
app.databases.use(.sqlite(.memory), as: .sqlite)
```
public func configure(_ app: Application) async throws {
// All other configurations...

Then you can enhance your tests by using `autoMigrate()` and `autoRevert()` to manage the database schema and data lifecycle during testing:
if app.environment == .testing {
app.databases.use(.sqlite(.memory), as: .sqlite)
} else {
app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
}
}
```

By combining these methods, you can ensure that each test starts with a fresh and consistent database state, making your tests more reliable and reducing the likelihood of false positives or negatives caused by lingering data.
!!! warning
Make sure you run your tests against the correct database, so you prevent accidentally overwriting data you do not want to lose.

Here's how the `withApp` function looks with the updated configuration:
Then you can enhance your tests by using `autoMigrate()` and `autoRevert()` to manage the database schema and data lifecycle during testing. To do so, you should create your own helper function `withAppIncludingDB` that includes the database schema and data lifecycles:

```swift
private func withApp(_ test: (Application) async throws -> ()) async throws {
private func withAppIncludingDB(_ test: (Application) async throws -> ()) async throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If @0xTim and @gwynne are fine with the name withAppIncludingDB, I'm fine too

let app = try await Application.make(.testing)
app.databases.use(.sqlite(.memory), as: .sqlite)
do {
try await configure(app)
try await app.autoMigrate()
Expand All @@ -162,6 +159,21 @@ private func withApp(_ test: (Application) async throws -> ()) async throws {
}
```

And then use this helper in your tests:
```swift
@Test func myDatabaseIntegrationTest() async throws {
try await withAppIncludingDB { app in
try await app.testing().test(.GET, "hello") { res async in
#expect(res.status == .ok)
#expect(res.body.string == "Hello, world!")
}
}
}
```

By combining these methods, you can ensure that each test starts with a fresh and consistent database state, making your tests more reliable and reducing the likelihood of false positives or negatives caused by lingering data.


## XCTVapor

Vapor includes a module named `XCTVapor` that provides test helpers built on `XCTest`. These testing helpers allow you to send test requests to your Vapor application programmatically or running over an HTTP server.
Expand Down