Thursday, February 2, 2023
Learning Code
  • Home
  • JavaScript
  • Java
  • Python
  • Swift
  • C++
  • C#
No Result
View All Result
  • Home
  • JavaScript
  • Java
  • Python
  • Swift
  • C++
  • C#
No Result
View All Result
Learning Code
No Result
View All Result
Home Swift

Declarative unit tests for Vapor

learningcode_x1mckf by learningcode_x1mckf
September 16, 2022
in Swift
0
Declarative unit tests for Vapor
74
SHARES
1.2k
VIEWS
Share on FacebookShare on Twitter


Writing exams utilizing XCTVapor

You might also like

The abstract Vapor service factory design pattern

SwiftNIO tutorial – The echo server

Introducing – Vapor cheatsheet – The.Swift.Dev.

In my earlier article I confirmed you the best way to construct a type safe RESTful API using Vapor. This time we will 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 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 utility, now we have to guarantee that if the app runs in testing mode we register an inMemory database as a substitute of our native SQLite file. We will merely alter the configuration and examine the surroundings and set the db driver based mostly 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 brief, however fairly helpful to study in regards to the fundamentals of testing Vapor endpoints. Sadly it will not let you know a lot about testing web sites or advanced API calls. ✅

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


@testable import App
import TodoApi
import Fluent
import XCTVapor

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

<swift>
    <p>As you may see first we setup & configure our utility, then we ship a GET request to the /todos/ endpoint. After now we have a response we are able to examine the standing code, the content material sort and we are able to attempt to decode the response physique as a legitimate paginated todo checklist 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

ultimate 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, luckily XCTVapor may also help us with the beforeRequest block. We will merely encode the enter object as a content material, then within the response handler we are able to examine 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 non-obligatory 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 a substitute of a .created worth. We will 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 = 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)
    )

Effectively, that is unhealthy, we should not be capable to create a todo merchandise with no title. We may use the built-in validation API to examine consumer enter, however actually talking that is not the very best method.

My problem with validation is that to start with you may’t return customized error messages and the opposite major purpose is that validation in Vapor shouldn’t be async by default. Ultimately you will face a scenario when it is advisable validate an object based mostly on a db name, then you may’t match that a part of the item validation course of into different non-async area validation. IMHO, this ought to be unified. 🥲

Fort the sake of simplicity we will begin with a customized validation technique, this time with none async logic concerned, in a while I will present you the best way 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, purpose: "Title is required")
        
    

Within the create controller we are able to merely name the throwing validateCreate perform, if one thing goes incorrect the Abort error can be returned as a response. It is usually doable 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 pieces was advantageous.


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 now we have to do is to replace our check case and examine for an error response.




struct ErrorResponse: Content material 
    let error: Bool
    let purpose: 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.purpose, "Title is required")
    )


Writing exams is an effective way to debug our server aspect Swift code and double examine our API endpoints. My solely problem with this method is that the code is not an excessive amount of self-explaining.

Declarative unit exams utilizing Spec

XCTVapor and the whole check framework works simply nice, however I had a small drawback with it. For those who ever labored with JavaScript or TypeScript you may need heard in regards to the SuperTest library. This little npm package deal offers us a declarative syntactical sugar for testing HTTP requests, which I favored method 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 offer a declarative API. Let me present you the way this works in apply, utilizing a real-world instance. 🙃


import PackageDescription

let package deal = Bundle(
    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? Effectively, we are able to 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() 
    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 finished"
    
    attempt 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 in a position to 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 method higher than it was within the earlier instance. We described the check situation then we set our expectations and eventually we run our check. With this format it’ll be extra simple to know check circumstances. For those who examine 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, another check 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

ultimate class AppTests: XCTestCase 

    
    
    non-public struct ErrorResponse: Content material 
        let error: Bool
        let purpose: 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 checklist 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() 
        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")
            .publish("/todos/")
            .physique(TodoCreateObject(title: ""))
            .count on(.badRequest)
            .count on(.json)
            .count on(ErrorResponse.self)  error in
                XCTAssertEqual(error.purpose, "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 finished"
        
        attempt 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() 
        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)
            .count on(.okay)
            .check()
    

That is how one can create an entire unit check situation for a REST API endpoint utilizing the Spec library. After all there are a dozen different points that we may repair, corresponding to higher enter object validation, unit check for the patch endpoint, higher exams for edge circumstances. Effectively, subsequent time. 😅

Through the use of 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 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 litte device that lets you write higher exams in your Swift backend apps. It has a really light-weight footprint, and the API is simple and simple to make use of. 💪




Source link

Share30Tweet19
learningcode_x1mckf

learningcode_x1mckf

Recommended For You

The abstract Vapor service factory design pattern

by learningcode_x1mckf
February 1, 2023
0
The abstract Vapor service factory design pattern

I've written a number of articles about manufacturing unit design patterns on my weblog and this time I might like to speak a couple of particular one, which...

Read more

SwiftNIO tutorial – The echo server

by learningcode_x1mckf
January 27, 2023
0
SwiftNIO tutorial – The echo server

Intoducing SwiftNIO In the event you used a excessive degree net framework, corresponding to Vapor, up to now, you would possibly had some interplay with occasion loops...

Read more

Introducing – Vapor cheatsheet – The.Swift.Dev.

by learningcode_x1mckf
January 23, 2023
0
Introducing – Vapor cheatsheet – The.Swift.Dev.

Out there on Gumroad Thanks for supporting my work by buying the cheatsheet. 🙏 Download now A whole Vapor framework reference for novices. greater than...

Read more

iCloud Programming Tutorial for iOS: An Introduction

by learningcode_x1mckf
January 18, 2023
0
iCloud Programming Tutorial for iOS: An Introduction

Editor’s observe: This week, we work with Ziad Tamim once more to provide you an introduction of iCloud programming. You’ll learn to save and retrieve knowledge from iCloud.On...

Read more

Easy multipart file upload for Swift

by learningcode_x1mckf
January 18, 2023
0
Easy multipart file upload for Swift

I imagine that you've got already heard in regards to the well-known multipart-data add method that everybody likes to add recordsdata and submit type knowledge, but when not,...

Read more
Next Post
An Introduction to JavaScript Service Workers

An Introduction to JavaScript Service Workers

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Related News

How to Display a Bottom Sheet Using SwiftUI

How to Display a Bottom Sheet Using SwiftUI

September 9, 2022
Beginner’s guide to Server side Swift using Vapor 4

Beginner’s guide to Server side Swift using Vapor 4

September 26, 2022
Deno team promises npm compatibility and ‘fastest JavaScript runtime web server ever built’ • DEVCLASS

Deno team promises npm compatibility and ‘fastest JavaScript runtime web server ever built’ • DEVCLASS

September 11, 2022

Browse by Category

  • C#
  • C++
  • Java
  • JavaScript
  • Python
  • Swift

RECENT POSTS

  • Java :Full Stack Developer – Western Cape saon_careerjunctionza_state
  • Pay What You Want for this Learn to Code JavaScript Certification Bundle
  • UPB Java Jam brings coffeehouse vibes to Taylor Down Under | Culture

CATEGORIES

  • C#
  • C++
  • Java
  • JavaScript
  • Python
  • Swift

© 2022 Copyright Learning Code

No Result
View All Result
  • Home
  • JavaScript
  • Java
  • Python
  • Swift
  • C++
  • C#

© 2022 Copyright Learning Code

Are you sure want to unlock this post?
Unlock left : 0
Are you sure want to cancel subscription?