From ce296bc8220311464083680422f28c2fe555d71b Mon Sep 17 00:00:00 2001 From: Ale Mohamad Date: Wed, 15 Jan 2025 22:26:12 +0100 Subject: [PATCH 1/6] Update Testing documentation --- docs/advanced/testing.md | 124 +++++++++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 32 deletions(-) diff --git a/docs/advanced/testing.md b/docs/advanced/testing.md index 4ee4fe226..796ec08fd 100644 --- a/docs/advanced/testing.md +++ b/docs/advanced/testing.md @@ -1,40 +1,50 @@ # Testing -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. +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. ## Getting Started -To use the `XCTVapor` module, ensure it has been added to your package's test target. +To use the `VaporTesting` module, ensure it has been added to your package's test target. ```swift let package = Package( ... dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") + .package(url: "https://github.com/vapor/vapor.git", from: "4.110.1") ], targets: [ ... .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), - .product(name: "XCTVapor", package: "vapor"), + .product(name: "VaporTesting", package: "vapor"), ]) ] ) ``` -Then, add `import XCTVapor` at the top of your test files. Create classes extending `XCTestCase` to write test cases. +Then, add `import VaporTesting` and `import Testing` at the top of your test files. Create structs with a `@Suite` name to write test cases. ```swift -import XCTVapor - -final class MyTests: XCTestCase { - func testStub() throws { +@testable import App +import VaporTesting +import Testing + +@Suite("App Tests") +struct AppTests { + @Test("Test Stub") + func stub() async throws { // Test here. } } ``` -Each function beginning with `test` will run automatically when your app is tested. +Each function marked with `@Test` will run automatically when your app is tested. + +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: + +```swift +@Suite("App Tests with DB", .serialized) +``` ### Running Tests @@ -42,52 +52,69 @@ Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swif ## Testable Application -Initialize an instance of `Application` using the `.testing` environment. You must call `app.shutdown()` before this application deinitializes. -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`. +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. + +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`. ```swift -let app = Application(.testing) -defer { app.shutdown() } -try configure(app) +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() +} ``` -Pass the `Application` to your package's `configure(_:)` method to apply your configuration. Any test-only configurations can be applied after. +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 `test` method. +To send a test request to your application, use the `withApp` private method and inside use the `app.testing().test()` method: ```swift -try app.test(.GET, "hello") { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.body.string, "Hello, world!") +@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!") + } + } } ``` -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. +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. 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. ```swift -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") +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]) }) ``` -### Testable Method +### Testing Method -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. +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. ```swift // Use programmatic testing. -app.testable(method: .inMemory).test(...) +app.testing(method: .inMemory).test(...) // Run tests through a live HTTP server. -app.testable(method: .running).test(...) +app.testing(method: .running).test(...) ``` The `inMemory` option is used by default. @@ -95,5 +122,38 @@ The `inMemory` option is used by default. The `running` option supports passing a specific port to use. By default `8080` is used. ```swift -.running(port: 8123) +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. + +```swift +app.databases.use(.sqlite(.memory), as: .sqlite) +``` + +Then you can enhance your tests by using `autoMigrate()` and `autoRevert()` to manage the database schema and data lifecycle during testing: + +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. + +Here's how the `withApp` function looks with the updated configuration: + +```swift +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() +} ``` From df4a03dfae8a35790d7533323cb676cc217356dc Mon Sep 17 00:00:00 2001 From: Ale Mohamad Date: Fri, 17 Jan 2025 20:12:27 +0100 Subject: [PATCH 2/6] XCTest documentation from PR suggestions --- docs/advanced/testing.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/advanced/testing.md b/docs/advanced/testing.md index 796ec08fd..2cd26ea77 100644 --- a/docs/advanced/testing.md +++ b/docs/advanced/testing.md @@ -22,6 +22,9 @@ let package = Package( ) ``` +!!! warning + Be sure to use the corresponding testing module, as failing to do so can result in Vapor test failures not being properly reported. + Then, add `import VaporTesting` and `import Testing` at the top of your test files. Create structs with a `@Suite` name to write test cases. ```swift @@ -157,3 +160,40 @@ private func withApp(_ test: (Application) async throws -> ()) async throws { try await app.asyncShutdown() } ``` + +## Using XCTest + +If you prefer XCTest over the Swift Testing framework, or if a project requires compatibility with a more traditional approach, XCTest is fully supported. Here’s how you can get started: + +First ensure that the `XCTVapor` module has been added to your package's test target. + +```swift +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"), + ]) + ] +) +``` + +Then, add `import XCTVapor` at the top of your test files. Create classes extending `XCTestCase` to write tests cases. + +```swift +import XCTVapor + +final class MyTests: XCTestCase { + func testStub() throws { + // Test here. + } +} +``` + +!!! note + For newer projects or teams adopting Swift concurrency, `VaporTesting` is highly recommended due to its simplicity and integration with Vapor. From 74128ea840b505cc06f530770f72e87be2813b02 Mon Sep 17 00:00:00 2001 From: Ale Mohamad Date: Sat, 18 Jan 2025 00:03:53 +0100 Subject: [PATCH 3/6] XCTVapor from PR suggestions --- docs/advanced/testing.md | 92 ++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/docs/advanced/testing.md b/docs/advanced/testing.md index 2cd26ea77..20aee771e 100644 --- a/docs/advanced/testing.md +++ b/docs/advanced/testing.md @@ -1,8 +1,13 @@ # Testing +## VaporTesting + 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. -## Getting Started +!!! note + For newer projects or teams adopting Swift concurrency, `VaporTesting` is highly recommended due to its simplicity and integration with Vapor. + +### Getting Started To use the `VaporTesting` module, ensure it has been added to your package's test target. @@ -49,11 +54,11 @@ To ensure your tests run in a serialized manner (e.g., when testing with a datab @Suite("App Tests with DB", .serialized) ``` -### Running Tests +#### Running Tests Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swift test --enable-test-discovery` to test via the CLI. -## Testable Application +### 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. @@ -76,7 +81,7 @@ private func withApp(_ test: (Application) async throws -> ()) async throws { 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 +#### Send Request To send a test request to your application, use the `withApp` private method and inside use the `app.testing().test()` method: @@ -108,7 +113,7 @@ try await app.testing().test(.POST, "todos", beforeRequest: { req in }) ``` -### Testing Method +#### Testing Method 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. @@ -128,7 +133,7 @@ The `running` option supports passing a specific port to use. By default `8080` app.testing(method: .running(port: 8123)).test(...) ``` -### Database Integration Tests +#### Database Integration Tests Configure the database specifically for testing to ensure that your live database is never used during tests. @@ -161,11 +166,13 @@ private func withApp(_ test: (Application) async throws -> ()) async throws { } ``` -## Using XCTest +## 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. -If you prefer XCTest over the Swift Testing framework, or if a project requires compatibility with a more traditional approach, XCTest is fully supported. Here’s how you can get started: +### Getting Started -First ensure that the `XCTVapor` module has been added to your package's test target. +To use the `XCTVapor` module, ensure it has been added to your package's test target. ```swift let package = Package( @@ -183,7 +190,7 @@ let package = Package( ) ``` -Then, add `import XCTVapor` at the top of your test files. Create classes extending `XCTestCase` to write tests cases. +Then, add `import XCTVapor` at the top of your test files. Create classes extending `XCTestCase` to write test cases. ```swift import XCTVapor @@ -195,5 +202,66 @@ final class MyTests: XCTestCase { } ``` -!!! note - For newer projects or teams adopting Swift concurrency, `VaporTesting` is highly recommended due to its simplicity and integration with Vapor. +Each function beginning with `test` will run automatically when your app is tested. + +#### Running Tests + +Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swift test --enable-test-discovery` to test via the CLI. + +### Testable Application + +Initialize an instance of `Application` using the `.testing` environment. You must call `app.shutdown()` before this application deinitializes. +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`. + +```swift +let app = Application(.testing) +defer { app.shutdown() } +try configure(app) +``` + +Pass the `Application` to your package's `configure(_:)` method to apply your configuration. Any test-only configurations can be applied after. + +#### Send Request + +To send a test request to your application, use the `test` method. + +```swift +try app.test(.GET, "hello") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "Hello, world!") +} +``` + +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. + +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. + +```swift +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") +}) +``` + +#### Testable Method + +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. + +```swift +// Use programmatic testing. +app.testable(method: .inMemory).test(...) + +// Run tests through a live HTTP server. +app.testable(method: .running).test(...) +``` + +The `inMemory` option is used by default. + +The `running` option supports passing a specific port to use. By default `8080` is used. + +```swift +.running(port: 8123) +``` From 90f0e69499073f21f1216953951d7f3e950ce7c1 Mon Sep 17 00:00:00 2001 From: Ale Mohamad Date: Tue, 21 Jan 2025 15:59:23 +0100 Subject: [PATCH 4/6] Adjustments from PR comments. --- docs/advanced/testing.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/advanced/testing.md b/docs/advanced/testing.md index 20aee771e..713d3772f 100644 --- a/docs/advanced/testing.md +++ b/docs/advanced/testing.md @@ -5,7 +5,7 @@ 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. !!! note - For newer projects or teams adopting Swift concurrency, `VaporTesting` is highly recommended due to its simplicity and integration with Vapor. + For newer projects or teams adopting Swift concurrency, `Swift Testing` is highly recommended over `XCTest`. ### Getting Started @@ -54,10 +54,6 @@ To ensure your tests run in a serialized manner (e.g., when testing with a datab @Suite("App Tests with DB", .serialized) ``` -#### Running Tests - -Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swift test --enable-test-discovery` to test via the CLI. - ### 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. @@ -204,13 +200,10 @@ final class MyTests: XCTestCase { Each function beginning with `test` will run automatically when your app is tested. -#### Running Tests - -Use `cmd+u` with the `-Package` scheme selected to run tests in Xcode. Use `swift test --enable-test-discovery` to test via the CLI. - ### Testable Application Initialize an instance of `Application` using the `.testing` environment. You must call `app.shutdown()` before this application deinitializes. + 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`. ```swift From 41c13203e7b95a00158a42c58e11d88d33ce04e9 Mon Sep 17 00:00:00 2001 From: Ale Mohamad Date: Wed, 22 Jan 2025 10:41:20 +0100 Subject: [PATCH 5/6] Fix broken links --- docs/fluent/overview.es.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/fluent/overview.es.md b/docs/fluent/overview.es.md index 36aebf2c4..dd75ade9a 100644 --- a/docs/fluent/overview.es.md +++ b/docs/fluent/overview.es.md @@ -1,6 +1,6 @@ # Fluent -Fluent es un framework [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) para Swift. Aprovecha el sólido sistema de tipado de Swift para proporcionar una interfaz fácil de usar para el manejo de bases de datos. El uso de Fluent se centra en la creación de tipos de modelo que representan estructuras de datos en la base de datos. Estos modelos se utilizan para realizar operaciones de creación, lectura, actualización y eliminación en lugar de escribir consultas directas a la base de datos. +Fluent es un framework [ORM](https://es.wikipedia.org/wiki/Mapeo_relacional_de_objetos) para Swift. Aprovecha el sólido sistema de tipado de Swift para proporcionar una interfaz fácil de usar para el manejo de bases de datos. El uso de Fluent se centra en la creación de tipos de modelo que representan estructuras de datos en la base de datos. Estos modelos se utilizan para realizar operaciones de creación, lectura, actualización y eliminación en lugar de escribir consultas directas a la base de datos. ## Configuración @@ -174,7 +174,7 @@ app.databases.use(.mysql( MongoDB es una base de datos popular NoSQL y sin esquemas diseñada para los programadores. El controlador es compatible con todos los proveedores de alojamiento en la nube y con las instalaciones en un hospedaje propio a partir de la versión 3.4 y en adelante. !!! note "Nota" - Este controlador está impulsado por un cliente de MongoDB creado y mantenido por la comunidad llamado [MongoKitten](https://github.com/OpenKitten/MongoKitten). MongoDB mantiene un cliente oficial, [mongo-swift-driver](https://github.com/mongodb/mongo-swift-driver), junto con una integración de Vapor, mongodb-vapor. + Este controlador está impulsado por un cliente de MongoDB creado y mantenido por la comunidad llamado [MongoKitten](https://github.com/orlandos-nl/MongoKitten). MongoDB mantiene un cliente oficial, [mongo-swift-driver](https://github.com/mongodb/mongo-swift-driver), junto con una integración de Vapor, mongodb-vapor. Para usar MongoDB, se deben de agregar las siguientes dependencias al paquete. @@ -188,7 +188,7 @@ Para usar MongoDB, se deben de agregar las siguientes dependencias al paquete. Una vez que se hayan agregado las dependencias, configurar la base de datos con Fluent utilizando `app.databases.use` en `configure.swift`. -Para conectarse, se debe de usar una cadena de texto el formato de [conexión estándar URI](https://docs.mongodb.com/master/reference/connection-string/index.html) de MongoDB. +Para conectarse, se debe de usar una cadena de texto el formato de [conexión estándar URI](https://www.mongodb.com/docs/upcoming/reference/connection-string/) de MongoDB. ```swift import Fluent @@ -249,7 +249,7 @@ var id: UUID? Este campo debe usar el property wrapper `@ID`. Fluent recomienda usar `UUID` y el campo especial `.id` ya que esto es compatible con todos los controladores de Fluent. -Si se desea utilizar una clave o tipo de ID personalizado, se debe de usar la sobrecarga de [`@ID(custom:)`](model.md#custom-identifier). +Si se desea utilizar una clave o tipo de ID personalizado, se debe de usar la sobrecarga de [`@ID(custom:)`](model.md#identificador-personalizado). ### Campos @@ -467,7 +467,7 @@ self.$galaxy.id = galaxyID Al anteponer el nombre de la propiedad padre con `$`, se accede al envoltorio de propiedad subyacente. Esto es necesario para acceder al `@Field` interno que almacena el valor real del identificador. !!! seealso "Ver También" - Consultar la propuesta de Evolución de Swift sobre envoltorios de propiedad, para obtener más información: [[SE-0258] Property Wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) + Consultar la propuesta de Evolución de Swift sobre envoltorios de propiedad, para obtener más información: [[SE-0258] Property Wrappers](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0258-property-wrappers.md) A continuación, crear una migración para preparar la base de datos para manejar las `Star`. @@ -504,7 +504,7 @@ app.migrations.add(CreateGalaxy()) app.migrations.add(CreateStar()) ``` -Dado que las migraciones se ejecutan en orden y `CreateStar` hace referencia al esquema "galaxies", el orden es importante. Por último, [ejecuta las migraciones](#migrate) para preparar la base de datos. +Dado que las migraciones se ejecutan en orden y `CreateStar` hace referencia al esquema "galaxies", el orden es importante. Por último, [ejecuta las migraciones](#migrar) para preparar la base de datos. Agregar una ruta para crear nuevas estrellas. From 6feba6b363d2474929bb85bb77bd9def4bef3f69 Mon Sep 17 00:00:00 2001 From: Ale Mohamad Date: Wed, 22 Jan 2025 11:57:53 +0100 Subject: [PATCH 6/6] Added user agent and retry on link check config --- markdown-link-check-config.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/markdown-link-check-config.yml b/markdown-link-check-config.yml index 57ef2f0bd..28d6da7f4 100644 --- a/markdown-link-check-config.yml +++ b/markdown-link-check-config.yml @@ -1,4 +1,11 @@ { - "ignorePatterns": [{ "pattern": ".*(localhost|127.0.0.1).*" }], - "aliveStatusCodes": [429, 200] + "httpHeaders": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + }, + "retryCount": 3, + "retryOn": [403, 429, 500, 502, 503, 504], + "ignorePatterns": [ + { "pattern": ".*(localhost|127.0.0.1).*" } + ], + "aliveStatusCodes": [200, 206, 301, 302, 429] }