Skip to content

Commit e0f2bec

Browse files
committed
Update Testing documentation
1 parent 4a377cf commit e0f2bec

File tree

1 file changed

+92
-32
lines changed

1 file changed

+92
-32
lines changed

docs/advanced/testing.md

+92-32
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,159 @@
11
# Testing
22

3-
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.
3+
Vapor includes a module named `VaporTesting` that provides test helpers built on `Swift Testing`. These testing helpers allow you to send test requests to your Vapor application programmatically or running over an HTTP server.
44

55
## Getting Started
66

7-
To use the `XCTVapor` module, ensure it has been added to your package's test target.
7+
To use the `VaporTesting` module, ensure it has been added to your package's test target.
88

99
```swift
1010
let package = Package(
1111
...
1212
dependencies: [
13-
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
13+
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1")
1414
],
1515
targets: [
1616
...
1717
.testTarget(name: "AppTests", dependencies: [
1818
.target(name: "App"),
19-
.product(name: "XCTVapor", package: "vapor"),
19+
.product(name: "VaporTesting", package: "vapor"),
2020
])
2121
]
2222
)
2323
```
2424

25-
Then, add `import XCTVapor` at the top of your test files. Create classes extending `XCTestCase` to write test cases.
25+
Then, add `import VaporTesting` and `import Testing` at the top of your test files. Create structs with a `@Suite` name to write test cases.
2626

2727
```swift
28-
import XCTVapor
29-
30-
final class MyTests: XCTestCase {
31-
func testStub() throws {
28+
@testable import App
29+
import VaporTesting
30+
import Testing
31+
32+
@Suite("App Tests")
33+
struct AppTests {
34+
@Test("Test Stub")
35+
func stub() async throws {
3236
// Test here.
3337
}
3438
}
3539
```
3640

37-
Each function beginning with `test` will run automatically when your app is tested.
41+
Each function marked with `@Test` will run automatically when your app is tested.
42+
43+
To ensure your tests run in a serialized manner (e.g., when testing with a database), include the `.serialized` option in the test suite declaration:
44+
45+
```swift
46+
@Suite("App Tests with DB", .serialized)
47+
```
3848

3949
### Running Tests
4050

4151
Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swift test --enable-test-discovery` to test via the CLI.
4252

4353
## Testable Application
4454

45-
Initialize an instance of `Application` using the `.testing` environment. You must call `app.shutdown()` before this application deinitializes.
46-
The shutdown is necessary to help release the resources that the app has claimed. In particular it is important to release the threads the application requests at startup. If you do not call `shutdown()` 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`.
55+
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.
56+
57+
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`.
4758

4859
```swift
49-
let app = Application(.testing)
50-
defer { app.shutdown() }
51-
try configure(app)
60+
private func withApp(_ test: (Application) async throws -> ()) async throws {
61+
let app = try await Application.make(.testing)
62+
do {
63+
try await configure(app)
64+
try await test(app)
65+
}
66+
catch {
67+
try await app.asyncShutdown()
68+
throw error
69+
}
70+
try await app.asyncShutdown()
71+
}
5272
```
5373

54-
Pass the `Application` to your package's `configure(_:)` method to apply your configuration. Any test-only configurations can be applied after.
74+
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.
5575

5676
### Send Request
5777

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

6080
```swift
61-
try app.test(.GET, "hello") { res in
62-
XCTAssertEqual(res.status, .ok)
63-
XCTAssertEqual(res.body.string, "Hello, world!")
81+
@Test("Test Hello World Route")
82+
func helloWorld() async throws {
83+
try await withApp { app in
84+
try await app.testing().test(.GET, "hello") { res async in
85+
#expect(res.status == .ok)
86+
#expect(res.body.string == "Hello, world!")
87+
}
88+
}
6489
}
6590
```
6691

67-
The first two parameters are the HTTP method and URL to request. The trailing closure accepts the HTTP response which you can verify using `XCTAssert` methods.
92+
The first two parameters are the HTTP method and URL to request. The trailing closure accepts the HTTP response which you can verify using `#expect` macro.
6893

6994
For more complex requests, you can supply a `beforeRequest` closure to modify headers or encode content. Vapor's [Content API](../basics/content.md) is available on both the test request and response.
7095

7196
```swift
72-
try app.test(.POST, "todos", beforeRequest: { req in
73-
try req.content.encode(["title": "Test"])
74-
}, afterResponse: { res in
75-
XCTAssertEqual(res.status, .created)
76-
let todo = try res.content.decode(Todo.self)
77-
XCTAssertEqual(todo.title, "Test")
97+
let newDTO = TodoDTO(id: nil, title: "test")
98+
99+
try await app.testing().test(.POST, "todos", beforeRequest: { req in
100+
try req.content.encode(newDTO)
101+
}, afterResponse: { res async throws in
102+
#expect(res.status == .ok)
103+
let models = try await Todo.query(on: app.db).all()
104+
#expect(models.map({ $0.toDTO().title }) == [newDTO.title])
78105
})
79106
```
80107

81-
### Testable Method
108+
### Testing Method
82109

83-
Vapor's testing API supports sending test requests programmatically and via a live HTTP server. You can specify which method you would like to use by using the `testable` method.
110+
Vapor's testing API supports sending test requests programmatically and via a live HTTP server. You can specify which method you would like to use through the `testing` method.
84111

85112
```swift
86113
// Use programmatic testing.
87-
app.testable(method: .inMemory).test(...)
114+
app.testing(method: .inMemory).test(...)
88115

89116
// Run tests through a live HTTP server.
90-
app.testable(method: .running).test(...)
117+
app.testing(method: .running).test(...)
91118
```
92119

93120
The `inMemory` option is used by default.
94121

95122
The `running` option supports passing a specific port to use. By default `8080` is used.
96123

97124
```swift
98-
.running(port: 8123)
125+
app.testing(method: .running(port: 8123)).test(...)
126+
```
127+
128+
### Database Integration Tests
129+
130+
Configure the database specifically for testing to ensure that your live database is never used during tests.
131+
132+
```swift
133+
app.databases.use(.sqlite(.memory), as: .sqlite)
134+
```
135+
136+
Then you can enhance your tests by using `autoMigrate()` and `autoRevert()` to manage the database schema and data lifecycle during testing:
137+
138+
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.
139+
140+
Here's how the `withApp` function looks with the updated configuration:
141+
142+
```swift
143+
private func withApp(_ test: (Application) async throws -> ()) async throws {
144+
let app = try await Application.make(.testing)
145+
app.databases.use(.sqlite(.memory), as: .sqlite)
146+
do {
147+
try await configure(app)
148+
try await app.autoMigrate()
149+
try await test(app)
150+
try await app.autoRevert()
151+
}
152+
catch {
153+
try? await app.autoRevert()
154+
try await app.asyncShutdown()
155+
throw error
156+
}
157+
try await app.asyncShutdown()
158+
}
99159
```

0 commit comments

Comments
 (0)