Declarative unit checks for Vapor


Writing checks utilizing XCTVapor

In my earlier article I confirmed you the right way to construct a sort protected RESTful API utilizing Vapor. This time we’ll prolong that undertaking 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 have now to guarantee that if the app runs in testing mode we register an inMemory database as an alternative of our native SQLite file. We are able to merely alter the configuration and examine the surroundings and set the db driver primarily based on it.

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(_ app: Software) throws {

    if app.surroundings == .testing {
        app.databases.use(.sqlite(.reminiscence), as: .sqlite, isDefault: true)
    }
    else {
        app.databases.use(.sqlite(.file("Assets/db.sqlite")), as: .sqlite)
    }

    app.migrations.add(TodoMigration())
    strive app.autoMigrate().wait()

    strive 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 brief, however fairly helpful to study concerning the fundamentals of testing Vapor endpoints. Sadly it will not let you know a lot about testing web sites or advanced API calls. ✅

We will make a easy take a look at that checks the return sort for our Todo checklist endpoint.

@testable import App
import TodoApi
import Fluent
import XCTVapor

remaining class AppTests: XCTestCase {

    func testTodoList() throws {
        let app = Software(.testing)
        defer { app.shutdown() }
        strive configure(app)

        strive app.take a look at(.GET, "/todos/", afterResponse: { res in
            XCTAssertEqual(res.standing, .okay)
            XCTAssertEqual(res.headers.contentType, .json)
            _ = strive res.content material.decode(Web page<TodoListObject>.self)
        })
    }
}

As you’ll be able to see first we setup & configure our software, then we ship a GET request to the /todos/ endpoint. After we have now a response we will examine the standing code, the content material sort and we will attempt to decode the response physique as a sound paginated todo checklist 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() }
        strive configure(app)

        let title = "Write a todo tutorial"
        
        strive app.take a look at(.POST, "/todos/", beforeRequest: { req in
            let enter = TodoCreateObject(title: title)
            strive req.content material.encode(enter)
        }, afterResponse: { res in
            XCTAssertEqual(res.standing, .created)
            let todo = strive res.content material.decode(TodoGetObject.self)
            XCTAssertEqual(todo.title, title)
            XCTAssertEqual(todo.accomplished, false)
            XCTAssertEqual(todo.order, nil)
        })
    }
}

This time we would wish to submit a brand new TodoCreateObject as a POST information, thankfully XCTVapor may also help 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 examine the HTTP standing code (it needs to be created) decode the anticipated response object (TodoGetObject) and validate the sphere values.

I additionally up to date the TodoCreateObject, because it doesn’t make an excessive amount of sense to have an non-obligatory Bool discipline 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 an alternative 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 = strive 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() }
    strive configure(app)

    
    let title = ""
    
    strive app.take a look at(.POST, "/todos/", beforeRequest: { req in
        let enter = TodoCreateObject(title: title)
        strive req.content material.encode(enter)
    }, afterResponse: { res in
        XCTAssertEqual(res.standing, .created)
        let todo = strive res.content material.decode(TodoGetObject.self)
        XCTAssertEqual(todo.title, title)
        XCTAssertEqual(todo.accomplished, false)
        XCTAssertEqual(todo.order, nil)
    })
}

Effectively, that is unhealthy, we should not be capable of create a todo merchandise with no title. We might use the built-in validation API to examine person enter, however truthfully talking that is not the perfect strategy.

My concern with validation is that to start with you’ll be able to’t return customized error messages and the opposite principal motive is that validation in Vapor isn’t async by default. Ultimately you will face a state of affairs when you might want to validate an object primarily based on a db name, then you’ll be able to’t match that a part of the thing validation course of into different non-async discipline validation. IMHO, this needs to be unified. 🥲

Fort the sake of simplicity we’ll begin with a customized validation methodology, this time with none async logic concerned, in a while I am going to present you the right 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, motive: "Title is required")
        }
    }
}

Within the create controller we will merely name the throwing validateCreate perform, if one thing goes unsuitable the Abort error shall be returned as a response. It’s also 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 little thing was high quality.

func create(req: Request) throws -> EventLoopFuture<Response> {
    let enter = strive req.content material.decode(TodoCreateObject.self)
    strive 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 have now to do is to replace our take a look at case and examine for an error response.



struct ErrorResponse: Content material {
    let error: Bool
    let motive: String
}

func testCreateInvalidTodo() throws {
    let app = Software(.testing)
    defer { app.shutdown() }
    strive configure(app)
    
    strive app.take a look at(.POST, "/todos/", beforeRequest: { req in
        let enter = TodoCreateObject(title: "")
        strive req.content material.encode(enter)
    }, afterResponse: { res in
        XCTAssertEqual(res.standing, .badRequest)
        let error = strive res.content material.decode(ErrorResponse.self)
        XCTAssertEqual(error.motive, "Title is required")
    })
}

Writing checks is a good way to debug our server aspect Swift code and double examine our API endpoints. My solely concern with this strategy 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 downside with it. For those who ever labored with JavaScript or TypeScript you might need heard concerning the SuperTest library. This little npm package deal offers us a declarative syntactical sugar for testing HTTP requests, which I favored manner an excessive amount of to return to common XCTVapor-based take a look at instances.

That is the rationale 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 ways this works in follow, utilizing a real-world instance. 🙃


import PackageDescription

let package deal = Package deal(
    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? Effectively, we will create a brand new merchandise, then replace it and examine if the outcomes are legitimate.

import Spec


func testUpdateTodo() throws {
    let app = Software(.testing)
    defer { app.shutdown() }
    strive configure(app)
    
    
    var existingTodo: TodoGetObject?
    
    strive app
        .describe("A legitimate todo object ought to exists after creation")
        .put up("/todos/")
        .physique(TodoCreateObject(title: "pattern"))
        .count on(.created)
        .count on(.json)
        .count on(TodoGetObject.self) { existingTodo = $0 }
        .take a look at()

    XCTAssertNotNil(existingTodo)

    let updatedTitle = "Merchandise is finished"
    
    strive app
        .describe("Todo needs to be up to date")
        .put("/todos/" + existingTodo!.id.uuidString)
        .physique(TodoUpdateObject(title: updatedTitle, accomplished: true, order: 2))
        .count on(.okay)
        .count on(.json)
        .count on(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 capable of create a todo object, it’s the very same create expectation as we used to jot down with the assistance of the XCTVapor framework.

IMHO the general code high quality is manner higher than it was within the earlier instance. We described the take a look at state of affairs then we set our expectations and at last we run our take a look at. With this format it will be extra easy to know take a look at instances. For those who examine the 2 variations the create case the second is trivial to know, however within the first one you really need to take a deeper take a look at every line to know what is going on on.

Okay, yet another take a look at earlier than we cease, let me present you the right way to describe the delete endpoint. We will refactor our code a bit, since there are some duplications already.

@testable import App
import TodoApi
import Fluent
import Spec

remaining class AppTests: XCTestCase {

    
    
    personal struct ErrorResponse: Content material {
        let error: Bool
        let motive: String
    }

    @discardableResult
    personal func createTodo(app: Software, enter: TodoCreateObject) throws -> TodoGetObject {
        var existingTodo: TodoGetObject?

        strive app
            .describe("A legitimate todo object ought to exists after creation")
            .put up("/todos/")
            .physique(enter)
            .count on(.created)
            .count on(.json)
            .count on(TodoGetObject.self) { existingTodo = $0 }
            .take a look at()
        
        XCTAssertNotNil(existingTodo)

        return existingTodo!
    }
    
    
    
    func testTodoList() throws {
        let app = Software(.testing)
        defer { app.shutdown() }
        strive configure(app)
        
        strive app
            .describe("A legitimate todo checklist web page needs to be returned.")
            .get("/todos/")
            .count on(.okay)
            .count on(.json)
            .count on(Web page<TodoListObject>.self)
            .take a look at()
    }
    
    func testCreateTodo() throws {
        let app = Software(.testing)
        defer { app.shutdown() }
        strive configure(app)

        strive createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))
    }

    func testCreateInvalidTodo() throws {
        let app = Software(.testing)
        defer { app.shutdown() }
        strive configure(app)

        strive app
            .describe("An invalid title response needs to be returned")
            .put up("/todos/")
            .physique(TodoCreateObject(title: ""))
            .count on(.badRequest)
            .count on(.json)
            .count on(ErrorResponse.self) { error in
                XCTAssertEqual(error.motive, "Title is required")
            }
            .take a look at()
    }

    func testUpdateTodo() throws {
        let app = Software(.testing)
        defer { app.shutdown() }
        strive configure(app)
        
        let todo: TodoGetObject? = strive createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))

        let updatedTitle = "Merchandise is finished"
        
        strive app
            .describe("Todo needs to be up to date")
            .put("/todos/" + todo!.id.uuidString)
            .count on(.okay)
            .count on(.json)
            .physique(TodoUpdateObject(title: updatedTitle, accomplished: true, order: 2))
            .count on(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() }
        strive configure(app)
        
        let todo: TodoGetObject? = strive createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))

        strive app
            .describe("Todo needs to be up to date")
            .delete("/todos/" + todo!.id.uuidString)
            .count on(.okay)
            .take a look at()
    }
}

That is how one can create a whole unit take a look at state of affairs for a REST API endpoint utilizing the Spec library. In fact 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 instances. Effectively, subsequent time. 😅

Through the use of Spec you’ll be able to construct your expectations by describing the use case, then you’ll be able to place your expectations on the described “specification” run the connected validators. The great factor about this declarative strategy is the clear self-explaining format which you can 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 easy and simple to make use of. 💪

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles