18.1 C
London
Friday, September 20, 2024

Declarative unit assessments for Vapor


Writing assessments utilizing XCTVapor

In my earlier article I confirmed you easy methods to construct a kind protected RESTful API utilizing Vapor. This time we will prolong that mission a bit and write some assessments utilizing the Vapor testing device to find the underlying points within the API layer. First we will 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 have now to make it possible for if the app runs in testing mode we register an inMemory database as an alternative of our native SQLite file. We will merely alter the configuration and verify 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())
    attempt app.autoMigrate().wait()

    attempt 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 be taught concerning the fundamentals of testing Vapor endpoints. Sadly it will not let you know a lot about testing web sites or complicated API calls. ✅

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

@testable import App
import TodoApi
import Fluent
import XCTVapor

closing class AppTests: XCTestCase {

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

        attempt app.check(.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’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 verify the standing code, the content material kind and we will attempt to decode the response physique as a sound paginated todo listing merchandise object.

This check case was fairly easy, now let’s write a brand new unit check for the todo merchandise creation.

@testable import App
import TodoApi
import Fluent
import XCTVapor

closing class AppTests: XCTestCase {

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

        let title = "Write a todo tutorial"
        
        attempt app.check(.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 wish to submit a brand new TodoCreateObject as a POST information, thankfully XCTVapor will help us with the beforeRequest block. We will merely encode the enter object as a content material, then within the response handler we will verify the HTTP standing code (it must 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-compulsory Bool subject 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 check will nonetheless fail, as a result of we’re returning an .okay standing as an alternative of a .created worth. We will 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.check(.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)
    })
}

Properly, that is dangerous, we should not be capable of create a todo merchandise and not using a title. We may use the built-in validation API to verify person enter, however actually talking that is not the most effective method.

My concern with validation is that initially you’ll be able to’t return customized error messages and the opposite primary cause is that validation in Vapor is just not async by default. Finally you may 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 article validation course of into different non-async subject validation. IMHO, this must be unified. 🥲

Fort the sake of simplicity we will begin with a customized validation methodology, this time with none async logic concerned, in a while I will present you easy methods to construct a generic validation & error reporting mechanism in 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 unsuitable the Abort error will likely be returned as a response. It’s also attainable 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 positive.

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 have now to do is to replace our check 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.check(.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 assessments is an effective way to debug our server facet Swift code and double verify our API endpoints. My solely concern with this method is that the code is not an excessive amount of self-explaining.

Declarative unit assessments utilizing Spec XCTVapor and your complete check framework works simply nice, however I had a small downside with it. For those who ever labored with JavaScript or TypeScript you may need heard concerning the SuperTest library. This little npm package deal offers us a declarative syntactical sugar for testing HTTP requests, which I preferred manner an excessive amount of to return to common XCTVapor-based check circumstances.

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 offer a declarative API. Let me present you ways this works in apply, utilizing a real-world instance. 🙃


import PackageDescription

let package deal = 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 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")
        .publish("/todos/")
        .physique(TodoCreateObject(title: "pattern"))
        .count on(.created)
        .count on(.json)
        .count on(TodoGetObject.self) { existingTodo = $0 }
        .check()

    XCTAssertNotNil(existingTodo)

    let updatedTitle = "Merchandise is completed"
    
    attempt app
        .describe("Todo must 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 manner higher than it was within the earlier instance. We described the check state of affairs then we set our expectations and eventually we run our check. With this format it will be extra simple to know check circumstances. For those who evaluate the 2 variations the create case the second is trivial to know, however within the first one you truly must take a deeper take a look at every line to know what is going on on.

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

closing 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")
            .publish("/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() }
        attempt configure(app)
        
        attempt app
            .describe("A legitimate todo listing web page must 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() }
        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 must be returned")
            .publish("/todos/")
            .physique(TodoCreateObject(title: ""))
            .count on(.badRequest)
            .count on(.json)
            .count on(ErrorResponse.self) { error in
                XCTAssertEqual(error.cause, "Title is required")
            }
            .check()
    }

    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 must 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() }
        attempt configure(app)
        
        let todo: TodoGetObject? = attempt createTodo(app: app, enter: TodoCreateObject(title: "Write a todo tutorial"))

        attempt app
            .describe("Todo must be up to date")
            .delete("/todos/" + todo!.id.uuidString)
            .count on(.okay)
            .check()
    }
}

That is how one can create a whole unit check state of affairs for a REST API endpoint utilizing the Spec library. After all there are a dozen different points that we may repair, similar to higher enter object validation, unit check for the patch endpoint, higher assessments for edge circumstances. Properly, 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 hooked up validators. The great factor about this declarative method 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 imagine that Spec is a enjoyable little device that lets you write higher assessments in your Swift backend apps. It has a really light-weight footprint, and the API is simple and straightforward to make use of. 💪

Latest news
Related news

LEAVE A REPLY

Please enter your comment!
Please enter your name here