|
1 | 1 | # Testing
|
2 | 2 |
|
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. |
4 | 4 |
|
5 | 5 | ## Getting Started
|
6 | 6 |
|
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. |
8 | 8 |
|
9 | 9 | ```swift
|
10 | 10 | let package = Package(
|
11 | 11 | ...
|
12 | 12 | 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") |
14 | 14 | ],
|
15 | 15 | targets: [
|
16 | 16 | ...
|
17 | 17 | .testTarget(name: "AppTests", dependencies: [
|
18 | 18 | .target(name: "App"),
|
19 |
| - .product(name: "XCTVapor", package: "vapor"), |
| 19 | + .product(name: "VaporTesting", package: "vapor"), |
20 | 20 | ])
|
21 | 21 | ]
|
22 | 22 | )
|
23 | 23 | ```
|
24 | 24 |
|
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. |
26 | 26 |
|
27 | 27 | ```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 { |
32 | 36 | // Test here.
|
33 | 37 | }
|
34 | 38 | }
|
35 | 39 | ```
|
36 | 40 |
|
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 | +``` |
38 | 48 |
|
39 | 49 | ### Running Tests
|
40 | 50 |
|
41 | 51 | Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swift test --enable-test-discovery` to test via the CLI.
|
42 | 52 |
|
43 | 53 | ## Testable Application
|
44 | 54 |
|
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`. |
47 | 58 |
|
48 | 59 | ```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 | +} |
52 | 72 | ```
|
53 | 73 |
|
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. |
55 | 75 |
|
56 | 76 | ### Send Request
|
57 | 77 |
|
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: |
59 | 79 |
|
60 | 80 | ```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 | + } |
64 | 89 | }
|
65 | 90 | ```
|
66 | 91 |
|
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. |
68 | 93 |
|
69 | 94 | 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.
|
70 | 95 |
|
71 | 96 | ```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]) |
78 | 105 | })
|
79 | 106 | ```
|
80 | 107 |
|
81 |
| -### Testable Method |
| 108 | +### Testing Method |
82 | 109 |
|
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. |
84 | 111 |
|
85 | 112 | ```swift
|
86 | 113 | // Use programmatic testing.
|
87 |
| -app.testable(method: .inMemory).test(...) |
| 114 | +app.testing(method: .inMemory).test(...) |
88 | 115 |
|
89 | 116 | // Run tests through a live HTTP server.
|
90 |
| -app.testable(method: .running).test(...) |
| 117 | +app.testing(method: .running).test(...) |
91 | 118 | ```
|
92 | 119 |
|
93 | 120 | The `inMemory` option is used by default.
|
94 | 121 |
|
95 | 122 | The `running` option supports passing a specific port to use. By default `8080` is used.
|
96 | 123 |
|
97 | 124 | ```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 | +} |
99 | 159 | ```
|
0 commit comments