Tutorial: how to write tests with Vapor 2

In this tutorial I will show you how to test your routes based on the result of the CRUD-Tutorial I wrote earlier. If you want to do the CRUD-Tutorial first and come back later, you can find it here. If you want to skip the CRUD-Tutorial you can clone the result here and start rightaway 🚀 !


You can find the result of this test-tutorial on github: here.

1. Project Structure

You should have a project structure like this:

test-example/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Config+Setup.swift
│ │ ├── Droplet+Setup.swift
│ │ ├── Routes.swift
│ │ └── Models/
│ │ └── User.swift
│ └── Run/
├── Tests/
│ └── AppTests/
│ ├── UserRequestTest.swift
│ └── Utilities.swift
├── Config/
├── Public/
├── Dependencies/
└── Products/

If you have cloned the repo from the crud-tutorial, you will already have the file UserRequestTest.swift, in this case delete all the code inside it.

If you created the crud-project by following the crud-tutorial, you will have a project structure like this:

test-example/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Config+Setup.swift
│ │ ├── Droplet+Setup.swift
│ │ ├── Routes.swift
│ │ └── Models/
│ │ └── User.swift
│ └── Run/
├── Tests/
│ └── AppTests/
│ ├── PostControllerTests.swift
│ ├── RouteTests.swift
│ └── Utilities.swift
├── Config/
├── Public/
├── Dependencies/
└── Products/

In this case delete (move to trash)
PostControllerTests.swift
• RouteTests.swift 
and create an empty File named
• UserRequestTest.swift

test-example/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Config+Setup.swift
│ │ ├── Droplet+Setup.swift
│ │ ├── Routes.swift
│ │ └── Models/
│ │ └── User.swift
│ └── Run/
├── Tests/
│ └── AppTests/
│ ├── PostControllerTests.swift <-- DELETE
│ ├── RouteTests.swift <-- DELETE
│ ├── UserRequestTest.swift <-- CREATE
│ └── Utilities.swift
├── Config/
├── Public/
├── Dependencies/
└── Products/

2. Test: User is created

Inside Tests/AppTests/UserRequestTest.swift write:

import Vapor
import HTTP
import XCTest
// import App is needed in order to save User directly like 'try User(username: "Hero", age:23).save()'
@testable import App
class UserRequestTest: TestCase {
  // getting an instance of our drop with our configuration 
let drop = try! Droplet.testable()
  func testThatUserGetsCreated() throws {
    /// MARK: PREPARING
let un = "Tim", age = 21
let user = User(username: un, age: age)

let json = try user.makeJSON()
let reqBody = try Body(json)
    /// MARK: TESTING
let req = Request(method: .post, uri: "/user", headers: ["Content-Type": "application/json"], body: reqBody)
let res = try drop.testResponse(to: req)
    // response is 200
res.assertStatus(is: .ok)
    // test response is json
guard let resJson = res.json else {
XCTFail("Error getting json from res: \(res)")
return
}
    try res.assertJSON("id", passes: { jsonVal in jsonVal.int != nil })
try res.assertJSON("username", equals: un)
try res.assertJSON("age", equals: age)
    /// MARK: CLEANUP
guard let userId = resJson["id"]?.int, let userToDelete = try User.find(userId) else {
XCTFail("Error could not convert id to int OR could not find user with id from response: \(res)")
return
}
    try userToDelete.delete()
}
}

Let me explain for short (besides the comments) what the code above does. I have developed a personal pattern how I structure test functions:

func testThatUserGetsCreated() throws {
  /// MARK: PREPARING
This marks the start of the area where I prepare all data I need in order to test
  /// MARK: TESTING
This marks the start of area where I actually test
  /// MARK: CLEANUP
This marks the start of the area where I clean up the eventually created data after testing is done
}

We want to test if our route /user when fired with a POST-Request sending a valid JSON is really creating a user.

FUNCTION:
I found to name the function after what they do very useful and expressive that’s why we call it testThatUserGetsCreated().

PREPARING:
In this area we are creating two variables un (username) and age (stands for age 😜). We initialize our User with these variables and make a JSON-Object out of him. We initialize a Body with the JSON-Object and that’s it.

TESTING:
We create a request object passing the http method, the url, header and body we need for the route. By firing up try drop.testResponse(to: req) our request is executed and we save the result in the variable res.
Now we can test if the response contains what we defined in route to return after successfully creating a user.

We defined that the route returns the created user as JSON.

So we check with guard if the response is containing json. If not we know something went wrong thus throwing an XCTFail. Then we check whether the JSON-Value at the key id is of type int. Also if the JSON-Value at the key username is equal to our un variable and if the JSON-Value at the key age is equal to our age variable. If all passes, awesome, let’s clean up!

CLEANUP:
Since the user got successfully created we will have an entry in our database. I think tests should leave places better than they found them 😉
So we convert the JSON-Value at the key id into an int and try to find the User with the id. If we got the user, we just execute delete() on him and we’re done ✌🏻

3. Run our first test

There are several ways to execute your test and they are all cool. In this tutorial we will use the vapor toolbox. Run in your terminal in your project directory:

vapor test

It will show you a cool ball going back and forth taking some seconds and then your test will pass! Whoop Whoop!

Hint: If you want a more verbose output go and execute instead: swift test

4. Test: User is Read

This function is smaller and since you know what means what here is the code 😊

import Vapor
import HTTP
import XCTest
// import App is needed in order to save User directly like 'try User(username: "Hero", age:23).save()'
@testable import App
class UserRequestTest: TestCase {
  // getting an instance of our drop with our configuration 
let drop = try! Droplet.testable()
  func testThatUserGetsCreated() throws {
...
}
  func testThatUserGetsReturned() throws {
    /// MARK: PREPARING
let un = "Elon", age = 31
let user = User(username: un, age: age)
    // creating user manually since we want to test to get him
try user.save()
    guard let userId = user.id?.int else {
XCTFail("Error converting user id to int")
return
}
    /// MARK: TESTING
let req = Request(method: .get, uri: "/user/\(userId)")
let res = try drop.testResponse(to: req)
    res.assertStatus(is: .ok)
try res.assertJSON("username", equals: un)
    /// MARK: CLEANUP
try user.delete()
}

}

You can again either go go for:

vapor test

Or what I prefer because you will see all functions that are executed when you run your tests:

swift test

5. Test: User is Updated

Talk is cheap. Show me the code.

import Vapor
import HTTP
import XCTest
// import App is needed in order to save User directly like 'try User(username: "Hero", age:23).save()'
@testable import App
class UserRequestTest: TestCase {
  // getting an instance of our drop with our configuration 
let drop = try! Droplet.testable()
  func testThatUserGetsCreated() throws {
...
}
  func testThatUserGetsReturned() throws {
...
}
  func testThatUserGetsUpdated() throws {
    /// MARK: PREPARING
let un = "Steve", age = 37
let user = User(username: un, age: age)
    try user.save()
    /// MARK: TESTING
guard let userId = user.id?.int else {
XCTFail("Error converting user id to int")
return
}
    // change data
let newUn = "Craig"
user.username = newUn
    let json = try user.makeJSON()
let reqBody = try Body(json)
    // test user gets updated
let updateUserReq = Request(method: .put, uri: "/user/\(userId)", headers: ["Content-Type": "application/json"], body: reqBody)
let updateUserRes = try drop.testResponse(to: updateUserReq)
    updateUserRes.assertStatus(is: .ok)
try updateUserRes.assertJSON("username", equals: newUn)
    /// MARK: CLEANUP
try user.delete()
}

}

Do you know what to execute in the command line 😉 ?

Hint: swift test or vapor test

6. Test: User is Deleted

I feel like no words are needed since I also comment my code. But if you feel explanations is missing please let me definitely know in the comment section!

import Vapor
import HTTP
import XCTest
// import App is needed in order to save User directly like 'try User(username: "Hero", age:23).save()'
@testable import App
class UserRequestTest: TestCase {
  // getting an instance of our drop with our configuration 
let drop = try! Droplet.testable()
  func testThatUserGetsCreated() throws {
...
}
  func testThatUserGetsReturned() throws {
...
}
  func testThatUserGetsUpdated() throws {
...
}
  func testThatUserGetsDeleted() throws {
    /// MARK: PREPARING
let user = User(username: "Jony", age: 23)
try user.save()
    guard let userId = user.id?.int else {
XCTFail("Error converting user id to int")
return
}
    /// MARK: TESTING
let req = Request(method: .delete, uri: "/user/\(userId)", headers: ["Content-Type":"application/json"], body: Body())
let res = try drop.testResponse(to: req)
    res.assertStatus(is: .ok)
try res.assertJSON("type", equals: "success")
}

}

And now the final hit in your terminal!

swift test

or you know it:

vapor test

Congrats! You successfully implemented tests with Vapor 🎉 !!


Thank you a lot for reading! If you have any suggestions or improvements let me know! I would love to hear from you! 😊

A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.