Home iOS & Swift Books Server-Side Swift with Vapor

11
Testing Written by Tim Condon

Testing 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 chapter, you’ll learn how to write tests for your Vapor applications. You’ll learn why testing is important and how it works with Swift Package Manager. Next, you’ll learn how to write tests for the TIL application from the previous chapters. Finally, you’ll see why testing matters on Linux and how to test your code on Linux using Docker.

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. Over the last several chapters, you’ve evolved and changed the TIL application. Testing every part of the application manually is slow and laborious, and this 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.

Writing tests with SwiftPM

On iOS, 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. On Linux, and with SwiftPM, there’s no Objective-C runtime. There’s also no Xcode project to remember schemes and which tests belong where.

.testTarget(name: "AppTests", dependencies: [
  .target(name: "App"),
  .product(name: "XCTVapor", package: "vapor"),
])

Testing users

Writing your first test

Create a new file in Tests/AppTests called UserTests.swift. This file will contain all the user-related tests. Open the new file and insert the following:

@testable import App
import XCTVapor

final class UserTests: XCTestCase {
}
func testUsersCanBeRetrievedFromAPI() throws {
  // 1
  let expectedName = "Alice"
  let expectedUsername = "alice"

  // 2
  let app = Application(.testing)
  // 3
  defer { app.shutdown() }
  // 4
  try configure(app)

  // 5
  let user = User(
    name: expectedName,
    username: expectedUsername)
  try user.save(on: app.db).wait()
  try User(name: "Luke", username: "lukes")
    .save(on: app.db)
    .wait()

  // 6
  try app.test(.GET, "/api/users", afterResponse: { response in
    // 7
    XCTAssertEqual(response.status, .ok)

    // 8
    let users = try response.content.decode([User].self)
    
    // 9
    XCTAssertEqual(users.count, 2)
    XCTAssertEqual(users[0].name, expectedName)
    XCTAssertEqual(users[0].username, expectedUsername)
    XCTAssertEqual(users[0].id, user.id)
  })
}
let databaseName: String
let databasePort: Int
// 1
if (app.environment == .testing) {
  databaseName = "vapor-test"
  databasePort = 5433
} else {
  databaseName = "vapor_database"
  databasePort = 5432
}
app.databases.use(.postgres(
  hostname: Environment.get("DATABASE_HOST")
    ?? "localhost",
  port: databasePort,
  username: Environment.get("DATABASE_USERNAME")
    ?? "vapor_username",
  password: Environment.get("DATABASE_PASSWORD")
    ?? "vapor_password",
  database: Environment.get("DATABASE_NAME")
    ?? databaseName
), as: .psql)
docker run --name postgres-test \
  -e POSTGRES_DB=vapor-test \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5433:5432 -d postgres
try app.autoRevert().wait()
try app.autoMigrate().wait()

Test extensions

The first test contains a lot of code that all tests need. 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
  }
}
@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
  }
}
let usersName = "Alice"
let usersUsername = "alicea"
let usersURI = "/api/users/"
var app: Application!
override func setUpWithError() throws {
  app = try Application.testable()
}
override func tearDownWithError() throws {
  app.shutdown()
}
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)
  })
}

Testing the User API

Open UserTests.swift and using the test helper methods add the following to test saving a user via the API:

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

Testing acronyms and categories

Open Models+Testable.swift and, at the bottom of the file, add a new extension to simplify creating 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
  }
}

Testing on Linux

Earlier in the chapter you learned why testing your application is important. For server-side Swift, testing on Linux is especially important. When you deploy your application to Heroku, for instance, you’re deploying to an operating system different from the one you used for development. It’s vital that you test your application on the same environment that you deploy it on.

Running tests in Linux

Running tests on Linux requires you to do things differently from running them on macOS. As mentioned earlier, the Objective-C runtime determines the test methods your XCTestCases provide. On Linux there’s no runtime to do this, so you must point Swift in the right direction. Swift 5.1 introduced test discovery, which parses your test classes to find tests to run.

# 1
FROM swift:5.2

# 2
WORKDIR /package
# 3
COPY . ./
# 4
CMD ["swift", "test", "--enable-test-discovery"]
# 1
version: '3'
# 2
services:
  # 3
  til-app:
    # 4
    depends_on:
      - postgres
    # 5
    build:
      context: .
      dockerfile: testing.Dockerfile
    # 6
    environment:
      - DATABASE_HOST=postgres
      - DATABASE_PORT=5432
  # 7
  postgres:
    # 8
    image: "postgres"
    # 9
    environment:
      - POSTGRES_DB=vapor-test
      - POSTGRES_USER=vapor_username
      - POSTGRES_PASSWORD=vapor_password
if (app.environment == .testing) {
  databaseName = "vapor-test"
  databasePort = 5433
} else {
if (app.environment == .testing) {
  databaseName = "vapor-test"
  if let testPort = Environment.get("DATABASE_PORT") {
    databasePort = Int(testPort) ?? 5433
  } else {
    databasePort = 5433
  }
} else {
# 1
docker-compose -f docker-compose-testing.yml build
# 2
docker-compose -f docker-compose-testing.yml up \
  --abort-on-container-exit

Where to go from here?

In this chapter, you learned how to test your Vapor applications to ensure they work correctly. Writing tests for your application also means you can run these tests on Linux. This gives you confidence your application will work when you deploy it. Having a good test suite allows you to evolve and adapt your applications quickly.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.