Writing checks utilizing XCTVapor
In my earlier article I confirmed you the best way to construct a kind protected RESTful API utilizing Vapor. This time we’ll lengthen that challenge a bit and write some checks utilizing the Vapor testing software to find the underlying points within the API layer. First we’ll use XCTVapor library, then we migrate to a light-weight declarative testing framework (Spec) constructed on prime of that.
Earlier than we begin testing our software, we’ve got to ensure that if the app runs in testing mode we register an inMemory database as a substitute of our native SQLite file. We are able to merely alter the configuration and verify the setting and set the db driver based mostly on it.
import Vapor
import Fluent
import FluentSQLiteDriver
public func configure(_ app: Software) throws {
if app.setting == .testing {
app.databases.use(.sqlite(.reminiscence), as: .sqlite, isDefault: true)
}
else {
app.databases.use(.sqlite(.file("Sources/db.sqlite")), as: .sqlite)
}
app.migrations.add(TodoMigration())
attempt app.autoMigrate().wait()
attempt TodoRouter().boot(routes: app.routes)
}
Now we’re able to create our very first unit take a look at utilizing the XCTVapor testing framework. The official docs are quick, however fairly helpful to be taught concerning the fundamentals of testing Vapor endpoints. Sadly it will not inform you a lot about testing web sites or complicated API calls. ✅
We’ll make a easy take a look at that checks the return kind for our Todo record endpoint.
@testable import App
import TodoApi
import Fluent
import XCTVapor
remaining class AppTests: XCTestCase {
func testTodoList() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
attempt app.take a look at(.GET, "/todos/", afterResponse: { res in
XCTAssertEqual(res.standing, .okay)
XCTAssertEqual(res.headers.contentType, .json)
_ = attempt res.content material.decode(Web page<TodoListObject>.self)
})
}
}
As you may see first we setup & configure our software, then we ship a GET request to the /todos/
endpoint. After we’ve got a response we will verify the standing code, the content material kind and we will attempt to decode the response physique as a legitimate paginated todo record merchandise object.
This take a look at case was fairly easy, now let’s write a brand new unit take a look at for the todo merchandise creation.
@testable import App
import TodoApi
import Fluent
import XCTVapor
remaining class AppTests: XCTestCase {
func testCreateTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
let title = "Write a todo tutorial"
attempt app.take a look at(.POST, "/todos/", beforeRequest: { req in
let enter = TodoCreateObject(title: title)
attempt req.content material.encode(enter)
}, afterResponse: { res in
XCTAssertEqual(res.standing, .created)
let todo = attempt res.content material.decode(TodoGetObject.self)
XCTAssertEqual(todo.title, title)
XCTAssertEqual(todo.accomplished, false)
XCTAssertEqual(todo.order, nil)
})
}
}
This time we would prefer to submit a brand new TodoCreateObject as a POST knowledge, happily XCTVapor can assist us with the beforeRequest block. We are able to merely encode the enter object as a content material, then within the response handler we will verify the HTTP standing code (it ought to be created) decode the anticipated response object (TodoGetObject) and validate the sector values.
I additionally up to date the TodoCreateObject, because it doesn’t make an excessive amount of sense to have an optionally available Bool area and we will use a default nil worth for the customized order. 🤓
public struct TodoCreateObject: Codable {
public let title: String
public let accomplished: Bool
public let order: Int?
public init(title: String, accomplished: Bool = false, order: Int? = nil) {
self.title = title
self.accomplished = accomplished
self.order = order
}
}
The take a look at will nonetheless fail, as a result of we’re returning an .okay
standing as a substitute of a .created
worth. We are able to simply repair this within the create methodology of the TodoController Swift file.
import Vapor
import Fluent
import TodoApi
struct TodoController {
func create(req: Request) throws -> EventLoopFuture<Response> {
let enter = attempt req.content material.decode(TodoCreateObject.self)
let todo = TodoModel()
todo.create(enter)
return todo
.create(on: req.db)
.map { todo.mapGet() }
.encodeResponse(standing: .created, for: req)
}
}
Now we should always attempt to create an invalid todo merchandise and see what occurs…
func testCreateInvalidTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
let title = ""
attempt app.take a look at(.POST, "/todos/", beforeRequest: { req in
let enter = TodoCreateObject(title: title)
attempt req.content material.encode(enter)
}, afterResponse: { res in
XCTAssertEqual(res.standing, .created)
let todo = attempt res.content material.decode(TodoGetObject.self)
XCTAssertEqual(todo.title, title)
XCTAssertEqual(todo.accomplished, false)
XCTAssertEqual(todo.order, nil)
})
}
Nicely, that is unhealthy, we should not have the ability to create a todo merchandise with out a title. We might use the built-in validation API to verify person enter, however actually talking that is not the most effective method.
My difficulty with validation is that to begin with you may’t return customized error messages and the opposite principal cause is that validation in Vapor shouldn’t be async by default. Ultimately you may face a scenario when that you must validate an object based mostly on a db name, then you may’t match that a part of the article validation course of into different non-async area validation. IMHO, this ought to be unified. 🥲
Fort the sake of simplicity we’ll begin with a customized validation methodology, this time with none async logic concerned, afterward I am going to present you the best way to construct a generic validation & error reporting mechanism on your JSON-based RESTful API.
import Vapor
import TodoApi
extension TodoModel {
func create(_ enter: TodoCreateObject) {
title = enter.title
accomplished = enter.accomplished
order = enter.order
}
static func validateCreate(_ enter: TodoCreateObject) throws {
guard !enter.title.isEmpty else {
throw Abort(.badRequest, cause: "Title is required")
}
}
}
Within the create controller we will merely name the throwing validateCreate perform, if one thing goes improper the Abort error will likely be returned as a response. It is usually doable to make use of an async methodology (return with an EventLoopFuture
) then await (flatMap
) the decision and return our newly created todo if every part was effective.
func create(req: Request) throws -> EventLoopFuture<Response> {
let enter = attempt req.content material.decode(TodoCreateObject.self)
attempt TodoModel.validateCreate(enter)
let todo = TodoModel()
todo.create(enter)
return todo
.create(on: req.db)
.map { todo.mapGet() }
.encodeResponse(standing: .created, for: req)
}
The very last thing that we’ve got to do is to replace our take a look at case and verify for an error response.
struct ErrorResponse: Content material {
let error: Bool
let cause: String
}
func testCreateInvalidTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
attempt app.take a look at(.POST, "/todos/", beforeRequest: { req in
let enter = TodoCreateObject(title: "")
attempt req.content material.encode(enter)
}, afterResponse: { res in
XCTAssertEqual(res.standing, .badRequest)
let error = attempt res.content material.decode(ErrorResponse.self)
XCTAssertEqual(error.cause, "Title is required")
})
}
Writing checks is an effective way to debug our server facet Swift code and double verify our API endpoints. My solely difficulty with this method is that the code is not an excessive amount of self-explaining.
Declarative unit checks utilizing Spec XCTVapor and your entire take a look at framework works simply nice, however I had a small drawback with it. If you happen to ever labored with JavaScript or TypeScript you may need heard concerning the SuperTest library. This little npm
package deal provides us a declarative syntactical sugar for testing HTTP requests, which I preferred means an excessive amount of to return to common XCTVapor-based take a look at circumstances.
That is the explanation why I’ve created the Spec “micro-framework”, which is actually one file with with an additional skinny layer round Vapor’s unit testing framework to supply a declarative API. Let me present you the way this works in apply, utilizing a real-world instance. 🙃
import PackageDescription
let package deal = Bundle(
identify: "myProject",
platforms: [
.macOS(.v10_15)
],
merchandise: [
.library(name: "TodoApi", targets: ["TodoApi"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor", from: "4.44.0"),
.package(url: "https://github.com/vapor/fluent", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.0.0"),
.package(url: "https://github.com/binarybirds/spec", from: "1.0.0"),
],
targets: [
.target(name: "TodoApi"),
.target(
name: "App",
dependencies: [
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Vapor", package: "vapor"),
.target(name: "TodoApi")
],
swiftSettings: [
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .launch))
]
),
.goal(identify: "Run", dependencies: [.target(name: "App")]),
.testTarget(identify: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
.product(name: "Spec", package: "spec"),
])
]
)
We had some expectations for the earlier calls, proper? How ought to we take a look at the replace todo endpoint? Nicely, we will create a brand new merchandise, then replace it and verify if the outcomes are legitimate.
import Spec
func testUpdateTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
var existingTodo: TodoGetObject?
attempt app
.describe("A legitimate todo object ought to exists after creation")
.put up("/todos/")
.physique(TodoCreateObject(title: "pattern"))
.anticipate(.created)
.anticipate(.json)
.anticipate(TodoGetObject.self) { existingTodo = $0 }
.take a look at()
XCTAssertNotNil(existingTodo)
let updatedTitle = "Merchandise is completed"
attempt app
.describe("Todo ought to be up to date")
.put("/todos/" + existingTodo!.id.uuidString)
.physique(TodoUpdateObject(title: updatedTitle, accomplished: true, order: 2))
.anticipate(.okay)
.anticipate(.json)
.anticipate(TodoGetObject.self) { todo in
XCTAssertEqual(todo.title, updatedTitle)
XCTAssertTrue(todo.accomplished)
XCTAssertEqual(todo.order, 2)
}
.take a look at()
}
The very first a part of the code expects that we had been in a position to create a todo object, it’s the very same create expectation as we used to write down with the assistance of the XCTVapor framework.
IMHO the general code high quality is means higher than it was within the earlier instance. We described the take a look at state of affairs then we set our expectations and eventually we run our take a look at. With this format it’ll be extra easy to grasp take a look at circumstances. If you happen to evaluate the 2 variations the create case the second is trivial to grasp, however within the first one you truly should take a deeper have a look at every line to grasp what is going on on.
Okay, yet one more take a look at earlier than we cease, let me present you the best way to describe the delete endpoint. We’ll refactor our code a bit, since there are some duplications already.
@testable import App
import TodoApi
import Fluent
import Spec
remaining class AppTests: XCTestCase {
non-public struct ErrorResponse: Content material {
let error: Bool
let cause: String
}
@discardableResult
non-public func createTodo(app: Software, enter: TodoCreateObject) throws -> TodoGetObject {
var existingTodo: TodoGetObject?
attempt app
.describe("A legitimate todo object ought to exists after creation")
.put up("/todos/")
.physique(enter)
.anticipate(.created)
.anticipate(.json)
.anticipate(TodoGetObject.self) { existingTodo = $0 }
.take a look at()
XCTAssertNotNil(existingTodo)
return existingTodo!
}
func testTodoList() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
attempt app
.describe("A legitimate todo record web page ought to be returned.")
.get("/todos/")
.anticipate(.okay)
.anticipate(.json)
.anticipate(Web page<TodoListObject>.self)
.take a look at()
}
func testCreateTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
attempt createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))
}
func testCreateInvalidTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
attempt app
.describe("An invalid title response ought to be returned")
.put up("/todos/")
.physique(TodoCreateObject(title: ""))
.anticipate(.badRequest)
.anticipate(.json)
.anticipate(ErrorResponse.self) { error in
XCTAssertEqual(error.cause, "Title is required")
}
.take a look at()
}
func testUpdateTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
let todo: TodoGetObject? = attempt createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))
let updatedTitle = "Merchandise is completed"
attempt app
.describe("Todo ought to be up to date")
.put("/todos/" + todo!.id.uuidString)
.anticipate(.okay)
.anticipate(.json)
.physique(TodoUpdateObject(title: updatedTitle, accomplished: true, order: 2))
.anticipate(TodoGetObject.self) { todo in
XCTAssertEqual(todo.title, updatedTitle)
XCTAssertTrue(todo.accomplished)
XCTAssertEqual(todo.order, 2)
}
.take a look at()
}
func testDeleteTodo() throws {
let app = Software(.testing)
defer { app.shutdown() }
attempt configure(app)
let todo: TodoGetObject? = attempt createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))
attempt app
.describe("Todo ought to be up to date")
.delete("/todos/" + todo!.id.uuidString)
.anticipate(.okay)
.take a look at()
}
}
That is how one can create an entire unit take a look at state of affairs for a REST API endpoint utilizing the Spec library. After all there are a dozen different points that we might repair, reminiscent of higher enter object validation, unit take a look at for the patch endpoint, higher checks for edge circumstances. Nicely, subsequent time. 😅
By utilizing Spec you may construct your expectations by describing the use case, then you may place your expectations on the described “specification” run the hooked up validators. The great factor about this declarative method is the clear self-explaining format which you could perceive with out taking an excessive amount of time on investigating the underlying Swift / Vapor code.
I consider that Spec is a enjoyable little software that lets you write higher checks on your Swift backend apps. It has a really light-weight footprint, and the API is simple and simple to make use of. 💪