Testing in Vapor 4

Use a pre-built Vapor application to learn both how to test your server-side Swift Vapor apps on macOS and also best practices to simplify your test code. By Tim Condon.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Test Extensions

The first test contains a lot of code that all tests need. You can extract the common parts to make the tests easier to read and to simplify future tests. In Tests/AppTests create a new file for one of these extensions, called Application+Testable.swift. Open the new file and add the following:

import XCTVapor
import App

extension Application {
  static func testable() throws -> Application {
    let app = Application(.testing)
    try configure(app)
    
    try app.autoRevert().wait()
    try app.autoMigrate().wait()

    return app
  }
}

This function allows you to create a testable Application object, configure it and set up the database. Next, create a new file in Tests/AppTests called Models+Testable.swift. Open the new file and create an extension to create a User:

@testable import App
import Fluent

extension User {
  static func create(
    name: String = "Luke",
    username: String = "lukes",
    on database: Database
  ) throws -> User {
    let user = User(name: name, username: username)
    try user.save(on: database).wait()
    return user
  }
}

This function saves a user, created with the supplied details, in the database. It has default values so you don’t have to provide any if you don’t care about them.

With all this created, you can now rewrite your user test. Open UserTests.swift and delete testUsersCanBeRetrievedFromAPI().

In UserTests create the common properties for all the tests:

let usersName = "Alice"
let usersUsername = "alicea"
let usersURI = "/api/users/"
var app: Application!

Next, implement setUpWithError() to run the code that must execute before each test:

override func setUpWithError() throws {
  app = try Application.testable()
}

This creates an Application for the test, which also resets the database.

Next, implement tearDownWithError() to shut the application down:

override func tearDownWithError() throws {
  app.shutdown()
}

Finally, rewrite testUsersCanBeRetrievedFromAPI() to use all the new helper methods:

func testUsersCanBeRetrievedFromAPI() throws {
  let user = try User.create(
    name: usersName, 
    username: usersUsername, 
    on: app.db)
  _ = try User.create(on: app.db)

  try app.test(.GET, usersURI, afterResponse: { response in
    XCTAssertEqual(response.status, .ok)
    let users = try response.content.decode([User].self)
    
    XCTAssertEqual(users.count, 2)
    XCTAssertEqual(users[0].name, usersName)
    XCTAssertEqual(users[0].username, usersUsername)
    XCTAssertEqual(users[0].id, user.id)
  })
}

This test does exactly the same as before but is far more readable. It also makes the next tests much easier to write. Run the tests again to ensure they still work.

Test executed with extensions

Test executed successfully

Testing the User API

In UserTests.swift, use the test helper methods to test saving a user via the API by adding the following test method:

func testUserCanBeSavedWithAPI() throws {
  // 1
  let user = User(name: usersName, username: usersUsername)
  
  // 2
  try app.test(.POST, usersURI, beforeRequest: { req in
    // 3
    try req.content.encode(user)
  }, afterResponse: { response in
    // 4
    let receivedUser = try response.content.decode(User.self)
    // 5
    XCTAssertEqual(receivedUser.name, usersName)
    XCTAssertEqual(receivedUser.username, usersUsername)
    XCTAssertNotNil(receivedUser.id)
    
    // 6
    try app.test(.GET, usersURI, 
      afterResponse: { secondResponse in
        // 7
        let users = 
          try secondResponse.content.decode([User].self)
        XCTAssertEqual(users.count, 1)
        XCTAssertEqual(users[0].name, usersName)
        XCTAssertEqual(users[0].username, usersUsername)
        XCTAssertEqual(users[0].id, receivedUser.id)
      })
  })
}

Here’s what the test does:

  1. Create a User object with known values.
  2. Use test(_:_:beforeRequest:afterResponse:) to send a POST request to the API
  3. Encode the request with the created user before you send the request.
  4. Decode the response body into a `User` object.
  5. Assert the response from the API matches the expected values.
  6. Make another request to get all the users from the API.
  7. Ensure the response only contains the user you created in the first request.

Run the tests to ensure that the new test works!

Test saving a user via the API.

API test successful

Next, add the following test to retrieve a single user from the API:

func testGettingASingleUserFromTheAPI() throws {
  // 1
  let user = try User.create(
    name: usersName, 
    username: usersUsername, 
    on: app.db)
  
  // 2
  try app.test(.GET, "\(usersURI)\(user.id!)", 
    afterResponse: { response in
      let receivedUser = try response.content.decode(User.self)
      // 3
      XCTAssertEqual(receivedUser.name, usersName)
      XCTAssertEqual(receivedUser.username, usersUsername)
      XCTAssertEqual(receivedUser.id, user.id)
    })
}

Here’s what the test does:

  1. Save a user in the database with known values.
  2. Get the user at /api/users/<USER ID>.
  3. Assert the values are the same as provided when creating the user.

The final part of the user’s API to test retrieves a user’s acronyms. Open Models+Testable.swift and, at the end of the file, create a new extension to create acronyms:

extension Acronym {
  static func create(
    short: String = "TIL",
    long: String = "Today I Learned",
    user: User? = nil,
    on database: Database
  ) throws -> Acronym {
    var acronymsUser = user

    if acronymsUser == nil {
      acronymsUser = try User.create(on: database)
    }

    let acronym = Acronym(
      short: short,
      long: long,
      userID: acronymsUser!.id!)
    try acronym.save(on: database).wait()
    return acronym
  }
}

This creates an acronym and saves it in the database with the provided values. If you don’t provide any values, it uses defaults. If you don’t provide a user for the acronym, it creates a user to use first.

Open UserTests.swift and create a method to test getting a user’s acronyms:

func testGettingAUsersAcronymsFromTheAPI() throws {
  // 1
  let user = try User.create(on: app.db)
  // 2
  let acronymShort = "OMG"
  let acronymLong = "Oh My God"
  
  // 3
  let acronym1 = try Acronym.create(
    short: acronymShort, 
    long: acronymLong, 
    user: user, 
    on: app.db)
  _ = try Acronym.create(
    short: "LOL", 
    long: "Laugh Out Loud", 
    user: user, 
    on: app.db)

  // 4
  try app.test(.GET, "\(usersURI)\(user.id!)/acronyms", 
    afterResponse: { response in
      let acronyms = try response.content.decode([Acronym].self)
      // 5
      XCTAssertEqual(acronyms.count, 2)
      XCTAssertEqual(acronyms[0].id, acronym1.id)
      XCTAssertEqual(acronyms[0].short, acronymShort)
      XCTAssertEqual(acronyms[0].long, acronymLong)
    })
}

Here’s what the test does:

  1. Create a user for the acronyms.
  2. Define some expected values for an acronym.
  3. Create two acronyms in the database using the created user. Use the expected values for the first acronym.
  4. Get the user’s acronyms from the API by sending a request to /api/users/<USER ID>/acronyms.
  5. Assert the response returns the correct number of acronyms and the first one matches the expected values.

Run the tests to ensure that the changes work!

Testing for a user’s acronyms.

User's acronyms test successful.

Testing Acronyms and Categories

Open Models+Testable.swift and, at the bottom of the file, add a new extension to simplify creating acronym categories:

extension App.Category {
  static func create(
    name: String = "Random",
    on database: Database
  ) throws -> App.Category {
    let category = Category(name: name)
    try category.save(on: database).wait()
    return category
  }
}

Like the other model helper functions, create(name:on:) takes the name as a parameter and creates a category in the database. The tests for the acronyms API and categories API are part of the starter project for this tutorial. Open CategoryTests.swift and uncomment all the code. The tests follow the same pattern as the user tests.

Open AcronymTests.swift and uncomment all the code. These tests also follow a similar pattern to before but there are some extra tests for the extra routes in the acronyms API. These include updating an acronym, deleting an acronym and the different Fluent query routes.

Run all the tests to make sure they all work. You should have a sea of green tests with every route tested!

All Tests Passing