is an important part of the software development process. Writing unit tests and automating them as much as possible allows you to develop and evolve your applications quickly.

In this server-side Swift tutorial, you’ll learn how to write tests for your applications. You’ll learn why testing is important, how it works with Swift Package Manager (SPM), and how to write tests for your application.

Why Should You Write Tests?

Software testing is as old as software development itself. Modern server applications are deployed many times a day, so it’s important that you’re sure everything works as expected. Writing tests for your application gives you confidence the code is sound.

Testing also gives you confidence when you refactor your code. Testing every part of your application manually is slow and laborious, even when your application is small! To develop new features quickly, you want to ensure the existing features don’t break. Having an expansive set of tests allows you to verify everything still works as you change your code.

Testing can also help you design your code. Test-driven development is a popular development process in which you write tests before writing code. This helps ensure you have full test coverage of your code. Test-driven development also helps you design your code and APIs.

Note: This tutorial assumes you have some experience with using Vapor to build web apps. See Getting Started with Server-side Swift with Vapor if you’re new to Vapor. This tutorial also assumes you have some experience working with the command line, Fluent, and Docker.

For information on using Fluent in Vapor, see Using Fluent and Persisting Models in Vapor.

If you’re new to Docker, check out Docker on macOS: Getting Started.

Getting Started

Download the starter project for this tutorial using the Download Materials button at the top or bottom of this tutorial.

The starter project contains a pre-built Vapor app named TIL (Today I Learned) that hosts user-supplied acronyms.

When testing on macOS, Xcode links tests to a specific test target. Xcode configures a scheme to use that target and you run your tests from within Xcode. The Objective-C runtime scans your XCTestCases and picks out the methods whose names begin with test. You’ll need another approach when testing server-side Swift on Linux, since there’s no Objective-C runtime. There’s also no Xcode project to remember schemes and which tests belong where.

To get started on macOS, open Package.swift in your project. There’s a test target defined in the targets array:


.testTarget(name: "AppTests", dependencies: ["App"]),

This defines a testTarget type with a dependency on App. Tests must live in the Tests/ directory. In this case, that’s Tests/AppTests.

From a Terminal in the root of the project, generate an Xcode project and open it with vapor xcode -y. If you select the TILApp-Package scheme, it’ll be set up with AppTests as a test target. You can run these tests as normal with Command-U, or Product ▸ Test:

Testing Users

Writing your First Test

Close your project in Xcode, then in Terminal, create a file for user-related tests:


touch Tests/AppTests/UserTests.swift
vapor xcode -y

This adds the file in the correct place in the directory hierarchy and regenerates the Xcode project to ensure the new file builds correctly. In Xcode, open UserTests.swift and add the following:


@testable import App
import Vapor
import XCTest
import FluentPostgreSQL

final class UserTests: XCTestCase {

}

This creates the XCTestCase you’ll use to test your users and imports the necessary modules to make everything work.

Next, add the following inside UserTests to test getting the users from the API:


func testUsersCanBeRetrievedFromAPI() throws {
  // 1
  let expectedName = "Alice"
  let expectedUsername = "alice"

  // 2
  var config = Config.default()
  var services = Services.default()
  var env = Environment.testing
  try App.configure(&config, &env, &services)
  let app = try Application(config: config, environment: env, services: services)
  try App.boot(app)

  // 3
  let conn = try app.newConnection(to: .psql).wait()

  // 4
  let user = User(name: expectedName, username: expectedUsername)
  let savedUser = try user.save(on: conn).wait()
  _ = try User(name: "Luke", username: "lukes").save(on: conn).wait()

  // 5
  let responder = try app.make(Responder.self)

  // 6
  let request = HTTPRequest(method: .GET, url: URL(string: "/api/users")!)
  let wrappedRequest = Request(http: request, using: app)

  // 7
  let response = try responder.respond(to: wrappedRequest).wait()

  // 8
  let data = response.http.body.data
  let users = try JSONDecoder().decode([User].self, from: data!)

  // 9
  XCTAssertEqual(users.count, 2)
  XCTAssertEqual(users[0].name, expectedName)
  XCTAssertEqual(users[0].username, expectedUsername)
  XCTAssertEqual(users[0].id, savedUser.id)

  // 10
  conn.close()
}

There’s a lot going on in this test; here’s the breakdown:

  1. Define some expected values for the test: a user’s name and username.
  2. Create an Application, similar to App.swift. This creates an entire Application object but doesn’t start running the application. This helps ensure you configure your real application correctly as your test calls the same App.configure(_:_:_:). Note, you’re using the .testing environment here.
  3. Create a database connection to perform database operations. Note the use of .wait() here and throughout the test. As you aren’t running the test on an EventLoop, you can use wait() to wait for the future to return. This helps simplify the code.
  4. Create a couple of users and save them in the database.
  5. Create a Responder type; this is what responds to your requests.
  6. Send a GET HTTPRequest to /api/users, the endpoint for getting all the users. A Request object wraps the HTTPRequest so there’s a Worker to execute it. Since this is a test, you can force unwrap variables to simplify the code.
  7. Send the request and get the response.
  8. Decode the response data into an array of Users.
  9. Ensure there are the correct number of users in the response and the users match those created at the start of the test.
  10. Close the connection to the database once the test has finished and stop the application to free up used resources properly.

Next, you must update your app’s configuration to support testing. Open configure.swift and below var databases = DatabasesConfig() add the following:


let databaseName: String
let databasePort: Int
// 1
if (env == .testing) {
  databaseName = "vapor-test"
  databasePort = 5433
} else {
  databaseName = "vapor"
  databasePort = 5432
}

This sets properties for the database name and port depending on the environment. You’ll use different names and ports for testing and running the application. Next, replace the call to PostgreSQLDatabaseConfig with the following:


let databaseConfig = PostgreSQLDatabaseConfig(
  hostname: "localhost",
  port: databasePort,
  username: "vapor",
  database: databaseName,
  password: "password")

This sets the database port and name from the properties set above. These changes allow you to run your tests on a database other than your production database. This ensures you start each test in a known state and don’t destroy live data.

The VaporTIL app was developed using Docker to host the app database. Setting up another database on the same machine for testing is straightforward. In Terminal, type the following:


docker run --name postgres-test -e POSTGRES_DB=vapor-test 
  -e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=password 
  -p 5433:5432 -d postgres

This changes the container name and database name. The Docker container is also mapped to host port 5433 to avoid conflicting with the existing database.

Run the tests and they should pass. However, if you run the tests again, they’ll fail. The first test run added two users to the database and the second test run now has four users since the database wasn’t reset.

Open configure.swift and add the following to the bottom of configure(_:_:_:):


var commandConfig = CommandConfig.default()
commandConfig.useFluentCommands()
services.register(commandConfig)

This adds the Fluent commands to your application, which allows you to manually run migrations. It also allows you to revert your migrations. Open UserTests.swift and, at the start of testUsersCanBeRetrievedFromAPI(), add the following:


// 1
let revertEnvironmentArgs = ["vapor", "revert", "--all", "-y"]
// 2
var revertConfig = Config.default()
var revertServices = Services.default()
var revertEnv = Environment.testing
// 3
revertEnv.arguments = revertEnvironmentArgs
// 4
try App.configure(&revertConfig, &revertEnv, &revertServices)
let revertApp = try Application(config: revertConfig, environment: revertEnv,
                                services: revertServices)
try App.boot(revertApp)
// 5
try revertApp.asyncRun().wait()

// 6
let migrateEnvironmentArgs = ["vapor", "migrate", "-y"]
var migrateConfig = Config.default()
var migrateServices = Services.default()
var migrateEnv = Environment.testing
migrateEnv.arguments = migrateEnvironmentArgs
try App.configure(&migrateConfig, &migrateEnv, &migrateServices)
let migrateApp = try Application(config: migrateConfig, environment: migrateEnv,
                                 services: migrateServices)
try App.boot(migrateApp)
try migrateApp.asyncRun().wait()

Here’s what this does:

  1. Set the arguments the Application should execute.
  2. Set up the services, configuration and testing environment.
  3. Set the arguments in the environment.
  4. Set up the application as earlier in the test. This creates a different Application object that executes the revert command.
  5. Call asyncRun() which starts the application and execute the revert command.
  6. Repeat the process again to run the migrations. This sets up the database on a separate connection, similar to how Vapor does it.

Build and run the tests again and this time they’ll pass!

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.

Close your project in Xcode then, in Terminal, create two new files for these extensions:


touch Tests/AppTests/Application+Testable.swift
touch Tests/AppTests/Models+Testable.swift
vapor xcode -y

When the project has regenerated, open Application+Testable.swift and add the following:


import Vapor
import App
import FluentPostgreSQL

extension Application {
  static func testable(envArgs: [String]? = nil) throws -> Application {
    var config = Config.default()
    var services = Services.default()
    var env = Environment.testing

    if let environmentArgs = envArgs {
      env.arguments = environmentArgs
    }

    try App.configure(&config, &env, &services)
    let app = try Application(config: config, environment: env, services: services)

    try App.boot(app)
    return app
  }
}

This function allows you to create a testable Application object. You can specify environment arguments, if required. This removes several lines of duplicated code in your test.

Underneath testable(envArgs:) add the following function to reset the database:


static func reset() throws {
  let revertEnvironment = ["vapor", "revert", "--all", "-y"]
  try Application.testable(envArgs: revertEnvironment).asyncRun().wait()
  let migrateEnvironment = ["vapor", "migrate", "-y"]
  try Application.testable(envArgs: migrateEnvironment).asyncRun().wait()
}

This uses the function above to create an application that runs the revert command and then runs the migrate command. This simplifies resetting the database in each test.

Next, add the following at the bottom of the file:


struct EmptyContent: Content {}

This defines an empty Content type to use when there’s no body to send in a request. Since you can’t define nil for a generic type, EmptyContent allows you to provide an type to satisfy the compiler.

Now, under reset(), add the following:


// 1
func sendRequest<T>(to path: String, method: HTTPMethod, headers: HTTPHeaders = .init(),
                    body: T? = nil) throws -> Response where T: Content {
  let responder = try self.make(Responder.self)
  // 2
  let request = HTTPRequest(method: method, url: URL(string: path)!,
                            headers: headers)
  let wrappedRequest = Request(http: request, using: self)
  // 3
  if let body = body {
    try wrappedRequest.content.encode(body)
  }
  // 4
  return try responder.respond(to: wrappedRequest).wait()
}

// 5
func sendRequest(to path: String, method: HTTPMethod, 
                 headers: HTTPHeaders = .init()) throws -> Response {
  // 6
  let emptyContent: EmptyContent? = nil
  // 7
  return try sendRequest(to: path, method: method, headers: headers,
                         body: emptyContent)
}

// 8
func sendRequest<T>(to path: String, method: HTTPMethod, headers: HTTPHeaders,
                    data: T) throws where T: Content {
  // 9
  _ = try self.sendRequest(to: path, method: method, headers: headers,
                           body: data)
}

Here’s what the code does:

  1. Define a method that sends a request to a path and returns a Response. Allow the HTTP method and headers to be set; this is for later tests. Also allow an optional, generic Content to be provided for the body.
  2. Create a responder, request and wrapped request as before.
  3. If the test contains a body, encode the body into the request’s content. Using Vapor’s encode(_:) allows you to take advantage of any custom encoders you set.
  4. Send the request and return the response.
  5. Define a convenience method that sends a request to a path without a body.
  6. Create an EmptyContent to satisfy the compiler for a body parameter.
  7. Use the method created previously to send the request.
  8. Define a method that sends a request to a path and accepts a generic Content type. This convenience method allows you to send a request when you don’t care about the response.
  9. Use the first method created above to send the request and ignore the response.

Underneath these helpers, add the following methods to get responses from a request:


// 1
func getResponse<C,T>(to path: String, method: HTTPMethod = .GET, 
                      headers: HTTPHeaders = .init(), data: C? = nil,
                      decodeTo type: T.Type) throws -> T where C: Content, T: Decodable {
  // 2
  let response = try self.sendRequest(to: path, method: method,
                                      headers: headers, body: data)
  // 3
  return try response.content.decode(type).wait()
}

// 4
func getResponse<T>(to path: String, method: HTTPMethod = .GET, 
                    headers: HTTPHeaders = .init(),
                    decodeTo type: T.Type) throws -> T where T: Decodable {
  // 5
  let emptyContent: EmptyContent? = nil
  // 6
  return try self.getResponse(to: path, method: method, headers: headers,
                              data: emptyContent, decodeTo: type)
}

Here’s what’s going on:

  1. Define a generic method that accepts a Content type and Decodable type to get a response to a request.
  2. Use the method created above to send the request.
  3. Decode the response body to the generic type and return the result.
  4. Define a generic convenience method that accepts a Decodable type to get a response to a request without providing a body.
  5. Create an empty Content to satisfy the compiler.
  6. Use the previous method to get the response to the request.

Next, open Models+Testable.swift and create an extension to create a User:


@testable import App
import FluentPostgreSQL

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

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!
var conn: PostgreSQLConnection!

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


override func setUp() {
  try! Application.reset()
  app = try! Application.testable()
  conn = try! app.newConnection(to: .psql).wait()
}

This reverts the database, generates an Application for the test, and creates a connection to the database.

Now implement teardown() to close the connection to the database and shut the application down:


override func tearDown() {
  conn.close()
  try? app.syncShutdownGracefully()
}

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


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

  let users = try app.getResponse(to: usersURI, decodeTo: [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.

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
  let receivedUser = try app.getResponse(
    to: usersURI,
    method: .POST,
    headers: ["Content-Type": "application/json"],
    data: user,
    decodeTo: User.self)

  // 3
  XCTAssertEqual(receivedUser.name, usersName)
  XCTAssertEqual(receivedUser.username, usersUsername)
  XCTAssertNotNil(receivedUser.id)

  // 4
  let users = try app.getResponse(to: usersURI, decodeTo: [User].self)

  // 5
  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 getResponse(to:method:headers:data:decodeTo:) to send a POST request to the API and get the response. Use the user object as the request body and set the headers correctly to simulate a JSON request. Convert the response into a User object.
  3. Assert the response from the API matches the expected values.
  4. Get all the users from API.
  5. Ensure the response only contains the user you created in the first request.

Run the tests to ensure that the new test works!

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: conn)
  // 2
  let receivedUser = try app.getResponse(to: "(usersURI)(user.id!)",
                                         decodeTo: 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 connection: PostgreSQLConnection) throws -> Acronym {
    var acronymsUser = user

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

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

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: conn)
  // 2
  let acronymShort = "OMG"
  let acronymLong = "Oh My God"
  // 3
  let acronym1 = try Acronym.create(short: acronymShort, long: acronymLong, 
                                    user: user, on: conn)
  _ = try Acronym.create(short: "LOL", long: "Laugh Out Loud", user: user, 
                         on: conn)

  // 4
  let acronyms = try app.getResponse(to: "(usersURI)(user.id!)/acronyms", 
                                     decodeTo: [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 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 connection: PostgreSQLConnection) throws -> App.Category {
    let category = Category(name: name)
    return try category.save(on: connection).wait()
  }
}

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!

Where to Go From Here?

You can download the completed project for this tutorial using the Download Materials button at the top or bottom of this tutorial.

In this tutorial, you learned how to test your Vapor applications to ensure they work correctly. Server-side Swift apps are typically deployed to Linux, and writing tests for your application means you can run these tests on Linux. This gives you confidence the application will work when you deploy it. Having a good test suite allows you to evolve and adapt your applications quickly. To learn about testing Vapor applications on Linux, see Server-Side Swift: Testing on Linux.

Vapor’s architecture has a heavy reliance on protocols. This, combined with Vapor’s dependency injection Service framework, makes testing simple and scalable. For large applications, you may even want to introduce a data abstraction layer so you aren’t testing with a real database.

This means you don’t have to connect to a database to test your main logic and will speed up the tests.

It’s important you run your tests regularly. Using a continuous integration (CI) system such as Jenkins or Bitbucket Pipelines allows you to test every commit. You must also keep your tests up to date.

Questions or comments on this tutorial? Leave them in the comments below!



Source link https://www.raywenderlich.com/1002044-testing-in-vapor

LEAVE A REPLY

Please enter your comment!
Please enter your name here