Skip to content

Commit

Permalink
Update Testing documentation (#1054)
Browse files Browse the repository at this point in the history
Following the issues #1038 and #594, this proposal suggests updating the
Testing documentation to include Swift Testing as an alternative to
XCTest.
  • Loading branch information
alemohamad authored Jan 22, 2025
1 parent 7a7183f commit c6b79d7
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 18 deletions.
181 changes: 171 additions & 10 deletions docs/advanced/testing.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,172 @@
# 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.

!!! note
For newer projects or teams adopting Swift concurrency, `Swift Testing` is highly recommended over `XCTest`.

### Getting Started

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.110.1")
],
targets: [
...
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "VaporTesting", package: "vapor"),
])
]
)
```

!!! 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
@testable import App
import VaporTesting
import Testing

@Suite("App Tests")
struct AppTests {
@Test("Test Stub")
func stub() async throws {
// Test here.
}
}
```

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)
```

### 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.

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
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. 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 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 `#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
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])
})
```

#### 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.

```swift
// Use programmatic testing.
app.testing(method: .inMemory).test(...)

// Run tests through a live HTTP server.
app.testing(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
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()
}
```

## 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.

## Getting Started
### Getting Started

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

Expand All @@ -29,20 +193,17 @@ import XCTVapor

final class MyTests: XCTestCase {
func testStub() throws {
// Test here.
// Test here.
}
}
```

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
### 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
Expand All @@ -53,7 +214,7 @@ 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
#### Send Request

To send a test request to your application, use the `test` method.

Expand All @@ -70,15 +231,15 @@ For more complex requests, you can supply a `beforeRequest` closure to modify he

```swift
try app.test(.POST, "todos", beforeRequest: { req in
try req.content.encode(["title": "Test"])
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
#### 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.

Expand Down
12 changes: 6 additions & 6 deletions docs/fluent/overview.es.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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.

Expand Down
11 changes: 9 additions & 2 deletions markdown-link-check-config.yml
Original file line number Diff line number Diff line change
@@ -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]
}

0 comments on commit c6b79d7

Please sign in to comment.