HomeiOS DevelopmentDeclarative unit exams for Vapor

Declarative unit exams for Vapor


Writing exams utilizing XCTVapor

In my earlier article I confirmed you the way to construct a kind protected RESTful API utilizing Vapor. This time we’ll prolong that undertaking a bit and write some exams utilizing the Vapor testing device 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 high of that.

Earlier than we begin testing our software, we’ve 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 verify the atmosphere and set the db driver based mostly on it.

import Vapor
import Fluent
import FluentSQLiteDriver

public func configure(_ app: Software) throws {

    if app.atmosphere == .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())
    strive app.autoMigrate().wait()

    strive TodoRouter().boot(routes: app.routes)
}


Now we’re able to create our very first unit check utilizing the XCTVapor testing framework. The official docs are quick, however fairly helpful to study concerning the fundamentals of testing Vapor endpoints. Sadly it will not inform you a lot about testing web sites or advanced API calls. ✅

We’ll make a easy check that checks the return kind for our Todo record endpoint.


@testable import App
import TodoApi
import Fluent
import XCTVapor

last class AppTests: XCTestCase {

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

        strive app.check(.GET, "/todos/", afterResponse: { res in
            XCTAssertEqual(res.standing, .okay)
            XCTAssertEqual(res.headers.contentType, .json)
            _ = strive res.content material.decode(Web page<TodoListObject>.self)
        })
    }
}
<swift>
    <p>As you possibly can see first we setup & configure our software, then we ship a GET request to the /todos/ endpoint. After we've a response we are able to verify the standing code, the content material kind and we are able to strive to decode the response physique as a sound paginated todo record merchandise object.</p>
    <p>This check case was fairly easy, now let's write a brand new unit check for the todo merchandise creation.</p>
    
<swift>
@testable import App
import TodoApi
import Fluent
import XCTVapor

last class AppTests: XCTestCase {

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

        let title = "Write a todo tutorial"
        
        strive app.check(.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 might prefer to submit a brand new TodoCreateObject as a POST knowledge, happily XCTVapor will 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 are able to verify the HTTP standing code (it ought 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 optionally available Bool area and we are able to 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 check 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 technique 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 must 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.check(.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)
    })
}

Properly, 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 consumer enter, however truthfully talking that is not the most effective method.

My subject with validation is that initially you possibly can’t return customized error messages and the opposite principal motive is that validation in Vapor will not be async by default. Ultimately you may face a scenario when it’s worthwhile to validate an object based mostly on a db name, then you possibly can’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 technique, this time with none async logic concerned, in a while I am going to present you the 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 are able to merely name the throwing validateCreate perform, if one thing goes improper the Abort error will probably be returned as a response. Additionally it is potential to make use of an async technique (return with an EventLoopFuture) then await (flatMap) the decision and return our newly created todo if all the things 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’ve to do is to replace our check case and verify 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.check(.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 exams is an effective way to debug our server facet Swift code and double verify our API endpoints. My solely subject with this method is that the code is not an excessive amount of self-explaining.

Declarative unit exams utilizing Spec

XCTVapor and your complete check 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 bundle provides us a declarative syntactical sugar for testing HTTP requests, which I preferred means an excessive amount of to return to common XCTVapor-based check 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 bundle = Package deal(
    title: "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(title: "Run", dependencies: [.target(name: "App")]),
        .testTarget(title: "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 check the replace todo endpoint? Properly, we are able to 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() }
    strive configure(app)
    
    
    var existingTodo: TodoGetObject?
    
    strive app
        .describe("A sound 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 }
        .check()

    XCTAssertNotNil(existingTodo)

    let updatedTitle = "Merchandise is finished"
    
    strive app
        .describe("Todo ought 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)
        }
        .check()
}

The very first a part of the code expects that we have been capable of 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 check situation then we set our expectations and at last we run our check. With this format it may be extra easy to grasp check 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 take a look at every line to grasp what is going on on.


Okay, yet one more check earlier than we cease, let me present you the 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

last class AppTests: XCTestCase {

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

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

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

        return existingTodo!
    }
    
    
    
    func testTodoList() throws {
        let app = Software(.testing)
        defer { app.shutdown() }
        strive configure(app)
        
        strive app
            .describe("A sound todo record web page ought to be returned.")
            .get("/todos/")
            .count on(.okay)
            .count on(.json)
            .count on(Web page<TodoListObject>.self)
            .check()
    }
    
    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 ought 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")
            }
            .check()
    }

    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 ought 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)
            }
            .check()
    }
    
    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 ought to be up to date")
            .delete("/todos/" + todo!.id.uuidString)
            .count on(.okay)
            .check()
    }
}

That is how one can create an entire unit check situation for a REST API endpoint utilizing the Spec library. In fact there are a dozen different points that we might repair, resembling higher enter object validation, unit check for the patch endpoint, higher exams for edge circumstances. Properly, subsequent time. 😅

By utilizing Spec you possibly can construct your expectations by describing the use case, then you possibly can place your expectations on the described “specification” run the hooked up validators. The good factor about this declarative method is the clear self-explaining format that you may perceive with out taking an excessive amount of time on investigating the underlying Swift / Vapor code.


I consider that Spec is a enjoyable litte device that lets you write higher exams on your Swift backend apps. It has a really light-weight footprint, and the API is simple and straightforward to make use of. 💪


RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments