Sending email from Swift Vapor

Like most services these days, my Vapor side project needs to send welcome emails when a user signs up for the service. While I could write my own SMTP client, it would still require a lot of DNS setup to make sure those emails made it through various spam countermeasures. Given this is a side project, and sending email is a minor component of it, that didn’t seem worthwhile to me. So I decided to use a 3rd party service who would take care of all of that.

I chose Mailgun for a couple of reasons:

  1. I’m hosting my side project on Heroku, and they provide a convenient Mailgun add on that I can provision.
  2. Mailgun offers a free tier, appropriate for my side project
  3. There’s already a Swift package for Mailgun’s API.

The Swift package for Mailgun got me most of the way there. However, I still needed a bit of infrastructure to render the emails, and then to validate the emails were correctly sent in my integration tests.

In this post I’m going to cover how I went about generating the email content, sending the email to Mailgun, and finally how I ensured I could test it all. I’m not going to cover how to set up Mailgun and all the DNS stuff because that’s very app specific, and is documented in Mailgun’s docs already.

Example use

I find it helpful when building a subsystem to sketch out what the use site will look like. It helps me determine if I’m building something that will meet my needs, and gives me a concrete target to build toward. With that in mind, I thought the ideal for my project was to define an email template for each kind of email I wanted to send, have a method that could render that down to a final email to send, then send it via an abstract email delivery service. The email body allows both text and html, and the body template was defined in Leaf.

Here’s how that looked in code:


struct WelcomeData: Encodable {
    let verifyLink: String

    init(user: User) {
        self.verifyLink = "myapp://verify/\(user.verifyToken)"
    }
}
...
let template = EmailTemplate(from: "donotreply@myapp.com",
                             to: email,
                             subject: "Welcome to MyApp!",
                             bodyTemplate: "WelcomeEmail")
let welcomeData = WelcomeData(user: user)
return template.render(welcomeData, on: container).flatMap { message -> Future<Void> in
    let deliveryService = try container.make(EmailDeliveryService.self)
    return deliveryService.send(message, on: container)
}

The first step is to create the EmailTemplate for the email I want to send and render it. The from, to, and subject properties are Strings that will be passed through unchanged to the final email. The bodyTemplate is the base name for the Leaf templates that will be rendered in the render() method. WelcomeData is the Leaf context for the templates; it defines anything that the the Leaf template will need. I like to think of it as a view model. It takes a model object and transforms it into values that the view (i.e. the Leaf template) needs. In this example, the WelcomeEmail template needs an email verification link, so WelcomeData constructs that based on a token assumed to be on the User model. To render the EmailTemplate into something that can be sent, render() is called, passing in WelcomeData.

The second step is to send the resulting EmailMessage. The dependency injection framework is asked to produce a EmailDeliveryService object, which can send an EmailMessage. EmailDeliveryService is a protocol, meaning it can be swapped out later, without the client code knowing or caring. This enables testing fakes to be injected during tests, as well as making it possible to move to a new email service should I ever decide to do that.

That covers the Swift code for creating and sending the email. I still need to define the Leaf templates though. I want to send both plain text and HTML MIME parts in my email, so regardless of the user’s email app they’ll see something reasonable. Since the email body template parts are Leaf templates, I put them in the standard location: Resources/Views. I also follow a naming convention so EmailTemplate.render() can find them at runtime.

Here’s the contents of WelcomeEmail.html.leaf:


<html>
<head></head>
<body>
<p>Hello!</p>

<p>Welcome to MyApp!</p>

<p><a href="#(verifyLink)">Verify your email address</a></p>

<p>The MyApp Team.</p>
</body>
</html>

And the contents of WelcomeEmail.text.leaf:


Hello!

Welcome to MyApp!

Verify your email address by clicking on this link: #(verifyLink)

The MyApp Team.

Both templates represent the same email, but the content changes based on the format they’re being rendered into. The #(verifyLink) placeholder is filled in with value in WelcomeData.verifyLink.

Now that I’ve defined my target API, I can start building the implementation.

Rendering email with Leaf

First I need to define what an email message is, because it is the type everything else is dependent on. The EmailTemplate needs to render to it, and EmailDeliveryService needs to send it. I decided to define my own types for this because it reduces coupling on a specific service, plus it more accurately represents what my app thinks an email is. Also, the necessary types are pretty simple, so I did’t think they’d increase my maintenance burden any.

Here’s my definition:


struct EmailBody {
    let text: String
    let html: String
}

struct EmailMessage {
    let from: String
    let to: String
    let subject: String
    let body: EmailBody
}

My app’s idea of an email is simple. It has a from, to, subject, and body fields. The only thing that might look out of the ordinary is the the body has two parts: HTML and plain text. My app doesn’t care about attachments or other features, so they don’t show up here.

With email defined, I could create the EmailTemplate which takes care of rendering Leaf templates down to a EmailMessage. I started by defining the properties of the EmailTemplate:


import Vapor
import Leaf
import TemplateKit

struct EmailTemplate {
    private let from: String
    private let to: String
    private let subject: String
    private let bodyTemplate: String

    init(from: String, to: String, subject: String, bodyTemplate: String) {
        self.from = from
        self.to = to
        self.subject = subject
        self.bodyTemplate = bodyTemplate
    }
    ...
}

The template is the same as the EmailMessage with the exception of bodyTemplate, which is the base name of the Leaf templates for the email body. Most of the work of EmailTemplate is to convert the bodyTemplate into a EmailBody. The top level render method looks like:


struct EmailTemplate {
    ...
    private static let htmlExtension = ".html"
    private static let textExtension = ".text"

    func render<E>(_ context: E, on container: Container) -> Future<EmailMessage> where E: Encodable {
        let htmlRender = render(bodyTemplate + EmailTemplate.htmlExtension, context, on: container)
        let textRender = render(bodyTemplate + EmailTemplate.textExtension, context, on: container)
        return htmlRender.and(textRender)
            .map { EmailBody(text: $1, html: $0) }
            .map { EmailMessage(from: self.from, to: self.to, subject: self.subject, body: $0) }
    }
    ...
}

The render() method takes the Leaf template context and a dependency injection container, and returns the promise of an EmailMessage. The implementation relies on a helper render() method that renders one Leaf template at a time. This top level render() calls it twice: once for the plain text template, and once for the html template. It uses the and operator to let the template renders run concurrently if possible, then combines the results into an EmailBody, before a final map that mixes in the from, to, and subject fields.

The helper render() method is similarly straight forward:


enum EmailError: Error {
    case invalidStringEncoding
    case emailProviderNotConfigured
}
...
struct EmailTemplate {
    ...
    private func render<E>(_ name: String, _ context: E, on container: Container) -> Future<String> where E: Encodable {
        do {
            let leaf = try container.make(LeafRenderer.self)
            return leaf.render(name, context).map { view in
                guard let str = String(data: view.data, encoding: .utf8) else {
                    throw EmailError.invalidStringEncoding
                }
                return str
            }
        } catch let error {
            return container.future(error: error)
        }
    }
}

The helper method here takes the full Leaf template name, its context, a dependency injection container and returns a promise of String, which is the fully rendered body. To achieve this, it creates a LeafRenderer and asks it to render the template. This results in view data, which it decodes into a String and returns. If any error is thrown, it’ll convert it into a rejected promise for convenience.

The email template rendering is fairly simple, but creating the EmailTemplate type reduces the boilerplate code needed for sending an email.

Sending email

I now have a fully rendered email message, and I need to send it. As I mentioned up top, I’m used a 3rd party Swift package to actually talk to Mailgun’s API. However, I wanted an easy way to inject testing fakes, and the ability to swap out email services later if necessary. So I’m first going to show how I integrated the Swift Mailgun package, then how I abstracted it with a generic interface that can be faked.

Since it’s a Swift package, I added it to my Package.swift file:


    dependencies: [
        ...
        .package(url: "https://github.com/twof/VaporMailgunService.git", from: "1.1.0"),
        ...
    ],
    ...
    targets: [
        ...
        .target(name: "App", dependencies: [
            ...
            "Mailgun",
            ...
            ]),
        ...
    ]

Running vapor update from the command line pulled down the package and updated all my dependencies. I decided to use a Provider to set up the Mailgun package in my app:


import Vapor
import Mailgun

struct MailgunProvider: Provider {
    private let config: MailgunConfigurationType

    init(mailgunConfig: MailgunConfigurationType) {
        self.config = mailgunConfig
    }

    func register(_ services: inout Services) throws {
        services.register(Mailgun(apiKey: config.apiKey, domain: config.domain), as: EmailDeliveryService.self)
    }

    func didBoot(_ container: Container) throws -> EventLoopFuture<Void> {
        return .done(on: container)
    }
}

There’s not much to this. The only interesting bit is the register() implementation. It registers the Mailgun service from the Mailgun framework as the implementation for the EmailDeliveryService protocol. It uses apiKey and domain fields from the configuration passed in, which will come from the appropriate environment configuration. In my case, since I’m using Heroku, the production environment configuration will pull from the MAILGUN_API_KEY and MAILGUN_DOMAIN environment variables. Additionally, the production configuration will take care of registering this provider.

I decided to use a provider pattern for the sake of the testing configuration. The production provider here doesn’t really need to be a provider; it only registers one service. But since the testing configuration does make full use of the Provider protocol, I decided to make the production configuration follow suite.

Now that I had Mailgun in my app, I needed to put it behind a generic protocol so all the client code could be agnostic about the email service being used. I started by defining a simple protocol:


import Vapor

protocol EmailDeliveryService: Service {
    func send(_ message: EmailMessage, on container: Container) -> Future<Void>
}

An EmailDeliveryService is a Service (in the dependency injection sense) that implements a send() method. The send() method takes an EmailMessage and returns a Void promise, which can be used to know if the send succeeded or failed.

For the final bit of sending an email, I need to conform the Mailgun service to my generic EmailDeliveryService protocol:


import Vapor
import Mailgun

extension Mailgun: EmailDeliveryService {
    func send(_ message: EmailMessage, on container: Container) -> Future<Void> {
        do {
            let mailgunMessage = Mailgun.Message(
                from: message.from,
                to: message.to,
                subject: message.subject,
                text: message.body.text,
                html: message.body.html
            )

            return try self.send(mailgunMessage, on: container).transform(to: ())
        } catch let error {
            return container.future(error: error)
        }
    }
}

The implementation is intentionally as thin as possible. This is partly because it’s hard to test protocol extensions. If I had needed anything more complicated, I would have opted to wrap Mailgun in another type that conformed to EmailDeliveryService. In any case, this simply converts my EmailMessage type into Mailgun’s version and sends it. It also wraps any thrown errors into a rejected promise for the convenience of any calling code.

And, with that, my app is now sending email via Mailgun! But is it sending the correct emails? Well…

Testing

The goal of a couple of design decisions was to make the testing of emails possible, or at least easier. The choice of abstracting out the email service into a generic protocol means I can inject a testing fake. Making the email template rendering separate from the email sending means I can still test the template rendering, even if I swap out the email service with a fake.

For my integration testing, I didn’t actually want to send any emails to Mailgun. That means my tests aren’t full integration tests, and won’t catch a misconfigured Mailgun setup. But I didn’t want my integration tests dependent on an external, non-local service to run; that would make them too flaky. Plus I’d likely run into an Mailgun API quota pretty quick. However, even with this limitation, I was able to verify that the correct emails got sent at the correct time.

Like with building the initial email types, I prefer to start out with what a final test might look like. Here’s a simplified test from my app that validates a welcome email was sent:


func testCreate_emailShouldSend() throws {
    app.emailDeliveryService.send_stubbed?.succeed()
    ...
    // Do something that should send an email
    ...
    XCTAssertTrue(app.emailDeliveryService.send_wasCalled)
    XCTAssertEqual(app.emailDeliveryService.send_wasCalled_withMessage!.to, "bob.jimbob@example.com")
    XCTAssertEqual(app.emailDeliveryService.send_wasCalled_withMessage!.from, "donotreply@myapp.com")
    XCTAssertEqual(app.emailDeliveryService.send_wasCalled_withMessage!.subject, "Welcome to MyApp!")

    let link = "myapp://verify/\(user!.verifyToken)"
    XCTAssertTrue(app.emailDeliveryService.send_wasCalled_withMessage!.body.html.contains(link))
    XCTAssertTrue(app.emailDeliveryService.send_wasCalled_withMessage!.body.text.contains(link))
}

The first line tells the email service testing fake that the next call to send() should return success. Next the test calls into the app in a way that should send a welcome email (as represented by the comment). The final lines assert that send was called, and with the correct parameters. The test also validates that the most important piece of information — the verify link — appears in both the plain text and HTML parts of the email message. I didn’t do a full text comparison because most of the body is static content, and comparing all of it makes the test more fragile than it needs to be.

With this test written, I can work backwards and define what my email service testing fake should implement.


import Vapor
import Mailgun

final class FakeEmailDeliveryService: EmailDeliveryService {
    var send_wasCalled = false
    var send_wasCalled_withMessage: EmailMessage?
    var send_stubbed: Promise<Void>?
    func send(_ message: EmailMessage, on container: Container) -> Future<Void> {
        send_wasCalled = true
        send_wasCalled_withMessage = message
        return send_stubbed!.futureResult
    }
    ...
}

The testing fake, FakeEmailDeliveryService, records if send() was called along with the EmailMessage it was called with. It only keeps track of the last message because my tests only send one at a time. The fake also has the ability to return a stubbed value from send(). This is useful for validating what happens if there’s a failure on the email service’s end. The fake send() assumes that the stubbed promise has been allocated elsewhere before it’s invoked.

Speaking of allocating the stubbed promise, there’s a bit of cleanup that’s required because of the way Vapor’s promises work:


final class FakeEmailDeliveryService: EmailDeliveryService {
    ...
    deinit {
        // Vapor does not like a promise created but not resolved before it is destroyed. It calls them "leaked" and crashes the tests. So make sure nothing is "leaked" in our tests.
        send_stubbed?.succeed()
    }
}

As the comment states, Vapor will crash a test if there are any promises left unresolved. This happens in any of my tests that don’t exercise the email functionality of the app. I could go through all of those tests and add code to resolve the send_stubbed promise, but that’d be verbose and tedious. Instead, I opted to have deinit forcefully resolve the promise if it hasn’t already been.

The FakeEmailDeliveryService needs to be registered with the dependency injection system so that code asking for a EmailDeliveryService will get an instance of it. As with the production version of EmailDeliveryService, I used a Provider:


struct TestMailProvider: Provider {
    var emailDeliveryService = FakeEmailDeliveryService()

    func register(_ services: inout Services) throws {
        services.register(emailDeliveryService, as: EmailDeliveryService.self)
    }

    func didBoot(_ container: Container) throws -> EventLoopFuture<Void> {
        emailDeliveryService.send_stubbed = container.eventLoop.newPromise()
        return .done(on: container)
    }
}

The first thing the provider does is create the FakeEmailDeliveryService and stash in a public member variable. It does this so tests can get ahold of it and validate the send() parameters, or resolve its return value. The register() method then registers the fake as the implementation of EmailDeliveryService. The didBoot() method takes care of creating the unresolved promise for send()‘s stubbed return value. Creating promises require an event loop, and didBoot() is the earliest place in the test code that I had access to one. I chose to allocate the stubbed promise this early because it allowed tests to assume its existence during set up without worrying about race conditions.

With the registering of TestMailProvider all of testing fakes are set up and ready to be used. However, FakeEmailDeliveryService wasn’t yet accessible to the test cases, which were expecting it as a property on TestApplication called emailDeliveryService. The rest of this section is showing the plumbing of TestMailProvider.emailDeliveryService property up through to TestApplication.

I started at the TestingConfiguration level, which is where the TestMailProvider is created and registered:


struct TestingConfiguration: ConfigurationType {
    ...
    let mailProvider = TestMailProvider()

    func configure(_ services: inout Services) throws {
        try services.register(mailProvider)
    }
    ...
}

This exposes TestMailProvider on the TestConfiguration, which TestApplication can then use:


class TestApplication {
    ...
    private let configuration: TestingConfiguration
    ...
    var emailDeliveryService: FakeEmailDeliveryService {
        return configuration.mailProvider.emailDeliveryService
    }
    ...
}

And now my tests could validate emails by using the FakeEmailDeliveryService exposed on TestApplication.

Conclusion

My Vapor side project needed to send emails on user registration, which could potentially be a complicated setup. Since email isn’t a major feature of my app, it made sense to delegate sending emails out to a third party service like Mailgun. Although there’s a convenient Mailgun Swift package, I still needed to build out infrastructure for rendering emails from Leaf templates and abstracting out the emailing sending so I can test my app’s email handling.

Basic Vapor logging with Papertrail

For my little Vapor side project, I need to have some visibility into what’s going on when it’s deployed. There are lots of solutions for this sort of thing, but since this is side project, I don’t need anything fancy. I think some basic logging infrastructure will be more than adequate for now. There are two parts to my logging “strategy.” First, I need to decided what to log out, and second, where to log it out.

As far as what to log out, I’m going to start with the basic request and response information. That is: the method and path of the request, the response status code, and how long it took for my server to process the request. This should give me some idea of the health of my server. I’m not going to log out request headers, or the request or response bodies. They could have sensitive information in them that I don’t want to end up in logs.

The question of where to log this information out to is a slightly more interesting question. I want a convenient place to get at the logs, plus the ability to easily search those logs. And, theoretically, if I needed to scale up my server to multiple instances (lol), I would want the logs all in one place. In the end, I decided on Papertrail for a couple of reasons:

  1. I’m hosting my side project on Heroku, and they provide a convenient Papertrail add on that I can trivially provision.
  2. Papertrail offers a free tier, appropriate for side projects
  3. Papertrail doesn’t require my app to know about it directly. Papertrail installs a daemon on the monitored system that grabs the syslog and sends it to its servers. Which means if my app logs out to syslog, Papertrail will take care of the rest.

My plan for adding logging to my Vapor 3 server: first, create a logger type that will log out to syslog. Second, create a Vapor middleware that will intercept requests and responses, and log those out to the logger created in step one. Finally, update the configuration to include the logging middleware.

Logging out to syslog

Logging out to syslog turned out to be mildly interesting because I’m developing in Swift on a macOS system, but deploying to a Linux system.

The first thing I found out about syslog is Apple considers it deprecated on macOS, and would really rather I use os_log. os_log does sound fancy, but it doesn’t exist on Linux, and Papertrail doesn’t use it. So I have to ignore Apple’s advice on this one. Secondly, while syslog() the function is exposed in Swift, it doesn’t have convenient bindings, so I had to create one.

Here’s the start of my Vapor syslog logger:


import Foundation
import Vapor

final class SyslogLogger: Logger {
    func log(_ string: String, at level: LogLevel, file: String, function: String, line: UInt, column: UInt) {
        let message = "\(string) [\(function)@\(file):\(line):\(column)]"
        syslog(message, at: level)
    }
    ...
}

Logger is a protocol that Vapor defines, and the log() method here is its only requirement. I format the message given all the logging information, and then send the message with the log level to a helper function. That helper function takes care of actually calling syslog():


final class SyslogLogger: Logger {
    ...
    private func syslog(_ message: String, at level: LogLevel) {
        message.withCString { cstring in
            withVaList([cstring]) { varArgs in
                vsyslog(level.syslogLevel, "%s", varArgs)
            }
        }
    }
}

I’m calling the vsyslog() variant of syslog because that’s the one exposed to Swift. It requires a C variable argument list. Fortunately, Swift provides a way to convert a Swift array into one of those with withVaList(). Since I’ve already formatted my message using Swift’s string interpolation, I don’t actually want any further formatting from vsyslog(). However, as Sven Weidauer points out, if message contains any formatting placeholders (e.g. %d), it would cause problems because vsyslog() would try to format them. Therefore, I use %s as the format string, and pass my message in the var args. However, there’s another wrinkle here. On iOS/macOS a String is considered a CVarArg and can be passed directly into withVaList(). Unfortunately, on Linux — where this code will be deployed — String is not a CVarArg. So I have to convert the Swift String to a C string with withCString before passing it into withVaList(). Finally, I convert Vapor’s LogLevel type into a level that syslog understands, and call vsyslog().

Here’s my log level mapping:


private extension LogLevel {
    var syslogLevel: Int32 {
        switch self {
        case .verbose: return LOG_DEBUG
        case .debug: return LOG_DEBUG
        case .info: return LOG_INFO
        case .warning: return LOG_WARNING
        case .error: return LOG_ERR
        case .fatal: return LOG_CRIT
        case .custom(_): return LOG_NOTICE
        }
    }
}

I’m not entirely happy with the log level mapping, but from what I can gather from the docs, this is as close of a mapping as any. The .custom(String) log level in Vapor is a bit puzzling, but I guess the authors felt they needed an escape hatch in case they forgot a level. The other perhaps slightly surprising bit is that .verbose and .debug both map to syslog’s idea of LOG_DEBUG, since syslog doesn’t have a counterpart to .
verbose
.

I have one final bit to my logger:


extension SyslogLogger: ServiceType {
    static func makeService(for worker: Container) throws -> SyslogLogger {
        return SyslogLogger()
    }
}

This is some boilerplate code to make my SyslogLogger type able to be created by Vapor’s dependency injection system by conforming it to the ServiceType protocol. It’ll be used when I go to configure logging for the app.

Logging requests and responses

Now that I have a logger that will send information to syslog, I need to take advantage of it. As I mentioned at the start, I want to log: a request HTTP method and path, the response status code, and how long it took to service the request. Fortunately, Vapor’s middleware provides a convenient place to monitor requests and their responses.

I’ll start with declaring my middleware type and defining the middleware entry point:


import Foundation
import Vapor

final class LogMiddleware: Middleware {
    private let logger: Logger

    init(logger: Logger) {
        self.logger = logger
    }

    func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
        let startTime = Date()
        return try next.respond(to: request).map { response in
            self.log(response, for: request, whichStartedAt: startTime)
            return response
        }
    }
    ...
}

My middleware class takes a generic Logger type in its init, so it can work any logger that I choose in the future. The respond(to:chainingTo:) method is the middleware entry point. The idea is to wrap the request processing so I know how long it takes. Once the request is processed and the response generated, then I can log all that information. To accomplish that, the code notes the current time, then calls through to the rest of stack to actually handle the request. It then grabs the response that results, and passes it along with the request and start time to a helper method called log():


final class LogMiddleware: Middleware {
    ...
    private func log(_ response: Response, for request: Request, whichStartedAt startTime: Date) {
        let requestInfo = "\(request.http.method.string) \(request.http.url.path)" + (request.http.url.query.map { "?\($0)" } ?? "")
        let responseInfo = "\(response.http.status.code) \(response.http.status.reasonPhrase)"
        let time = format(Date().timeIntervalSince(startTime))
        logger.info("\(requestInfo) -> \(responseInfo) [\(time)]")
    }
    ...
}

This is the method where I construct the message to be logged. I build it piecemeal by the three main parts: the request, the response, and the time it took. For the request, I put together the method, path, and the query parameters, if there are any. For the response, I put together the HTTP status code, plus the human readable version of the status code. Vapor helpfully provides that on the reasonPhrase property. Finally, I compute the time elapsed and format that. Once I have all three pieces, I put them together into one string. The resulting message looks something like GET /my/path?p1=y&p2 -> 200 OK [98.20ms].

Formatting time is a bit verbose, so I put that into its own method:


final class LogMiddleware: Middleware {
    ...
    private let intervalFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 2
        formatter.multiplier = 1000
        return formatter
    }()
    ...
    private func format(_ interval: TimeInterval) -> String {
        return intervalFormatter.string(for: interval).map { $0 + "ms" } ?? "???ms"
    }
}

I’m leveraging NumberFormatter to do all the hard work. I set it up to create decimal numbers with no more than 2 digits after the decimal point, and 1000 as a multiplier. That’s because I’m giving the formatter seconds, but I want it displayed in milliseconds. My format() method simply calls the NumberFormatter, and appends “ms” as the units. The trailing ?? "???ms" shouldn’t ever be executed, and is there to satisfy the compiler, since string(for:) can technically return nil.

The final bit for the logging middleware is to hook into the dependency injection system:


extension LogMiddleware: ServiceType {
    static func makeService(for worker: Container) throws -> LogMiddleware {
        return try LogMiddleware(logger: worker.make())
    }
}

This is also where the Logger used by the middleware will be instantiated and passed into the init method.

Configuring logging for the app

The last little bit is to register my logger and middleware with the dependency injection framework, and add the logging middleware in the correct order. That means modifying my configure() method:


/// Called before your application initializes.
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services, _ configuration: ConfigurationType) throws {
    ...
    services.register(LogMiddleware.self)
    services.register(SyslogLogger.self)
    ...
    /// Register middleware
    var middlewares = MiddlewareConfig()
    middlewares.use(LogMiddleware.self)
    ...
}

Here I register LogMiddleware and SyslogLogger with the dependency injection framework. Then, in the middleware configuration, I add my log middleware first, before all other middlewares. This will allow it to measure as much of the time it takes processing the request as possible. It will be the first middleware called when a request is received, and the last to be called when the response is returned.

Conclusion

While I need to have some visibility into how my Vapor server is doing in production, my side project doesn’t need anything overly complicated. I chose to use Papertrail because it provided an easy and convenient place to aggregate my logs. To get logs to Papertrail, I added middleware to my app that logs out basic request and response information to syslog.

How to do integration testing on a Vapor server

As I’m getting further into my Vapor side project, I’m learning how writing Swift iOS/macOS apps is different from writing Swift server apps. One of the ways they are different is integration tests. For iOS apps, it’s usually done via automated UI testing. For Vapor, it’s API testing, or sometimes I see it called controller testing. Regardless of its name, I find myself writing more integration tests than unit tests for my server. I think there are two reasons for that:

  1. APIs tend to be well defined and easy to interact with, at least compared to UIs
  2. The integration tests give me a lot more confidence than unit tests that my server is behaving that way I intended it to

Of course, the downside is integration tests are much slower to run. So far, that’s a trade off I’ve been willing to make.

In any case, I want to talk about how I set up integration testing for my Vapor 3 app. My goal was to write test cases where calling the API under test was simple, and looked as much like real client code as possible. To accomplish that, I wrote some helper types called TestApplication and TestResponse to hide boilerplate code. I also created some fixtures to easily seed the database with data, along with some utility methods to reset the database after each test. Finally, I had to figure out how use Docker to stand up a PostgreSQL database I could run the tests against.

Example testcase

I’m going to start with a test case showing how I wrote my integration tests, then work backwards explaining the infrastructure I set up to support that.

As an example, I’m going to test an API that allows an authenticated user to invalidate all auth tokens that have been issued for their account, thereby forcing all devices to re-authenticate. Here’s the test:


class UserControllerTests: XCTestCase {
    ...
    func testResetTokens_isTrue_success() throws {
        let request = UserController.UserEnvelope(user: UserController.UpdateRequest(resetTokens: true))
        let response = try app.put("/api/users/\(user.id!)", headers: .withAuthorization(app.apiKey, jwt), body: request)
        XCTAssertEqual(response.status, .ok)

        let responseBody = try response.content(decodeTo: UserController.UserEnvelope<UserController.UserResponse>.self).user

        XCTAssertEqual(responseBody.id, user.id!)
        XCTAssertEqual(responseBody.email, user.email)

        let showResponse = try app.get("/api/users/\(user.id!)", headers: .withAuthorization(app.apiKey, jwt))
        XCTAssertEqual(showResponse.status, .unauthorized)
    }
    ...
}

Right now I’m using XCTestCase because it doesn’t rely on any external dependencies. I’ve been entertaining the idea of using Quick & Nimble so I can share more setup between tests, use custom matchers, and make the assertions a touch more readable. But for now, I just name my test functions with a specific pattern: API name, plus parameter values, plus expected results. I also toss in the app state if it’s relevant for the test.

The first line of the test is creating the request that I want to validate. The app specific request types are currently defined in the controller code, which is why there’s namespacing. The next line makes the request via TestApplication and then waits for a response. The request is performing a PUT on /api/users/:user_id with the request parameters encoded in the body. The .withAuthorization(app.apiKey, jwt) is a fixture on HTTPHeaders that creates headers with a valid authorization for this user.

Once the API performs the token reset, it returns the user object. The test validates this by decoding the response body and asserting the id and email match what’s expected. Then, to fully verify that all tokens were reset, the test makes another request (this time a GET request) with the existing token. It asserts that the request is rejected with an unauthorized status.

At this point there are a lot of undefined things that make this test function. I will eventually get to them all. But for now, I’ll start with the setup code that creates the user and JWT that are used by this test. Here’s what I got for that:


import Foundation
import XCTest
import Vapor
import FluentPostgreSQL
import JWT
@testable import App

class UserControllerTests: XCTestCase {
    var app: TestApplication!
    var connection: PostgreSQLConnection!
    var user: User!
    var jwt: String!

    override func setUp() {
        super.setUp()

        try! TestApplication.reset()
        app = try! TestApplication()
        connection = app.connection

        user = User.user(on: connection)

        let payload = AuthJWTPayload(uid: UserIDClaim(value: user.id!),
                                     iat: IssuedAtClaim(value: Date()))
        jwt = try! JWTSerialization().encode(payload, on: app.container)
    }

    override func tearDown() {
        super.tearDown()

        _ = user.delete(on: connection)

        connection.close()
    }
    ...
}

Since I’m using XCTest, I’m using the standard setUp() and tearDown() methods. In the setup I start by resetting the app via TestApplication.reset(); this resets the database. Then I create a TestApplication instance, which is the test’s interface into the app. I’ll need to add data to the database as well verify data was created, so I go ahead and create a database connection. Then I immediately use it create a user using the User.user(on:) fixture. The final bit of setup is creating a valid JWT for the user that can be used for making API calls. I wrote about using JWTs in Vapor API servers earlier.

My tearDown() is simple. I delete the user I created in the set up, and then close the database connection. This is a bit redundant in that I reset the app on each test, but I like to do it because it feels like good hygiene.

Now I have a complete test case, but there’s still a lot of infrastructure to build. The example test above used a couple of fixtures, so I’ll talk about those next.

Fixtures

My goal with fixtures is to provide a quick and easy way to create model data to test with. My fixtures tend to change a lot from app to app because they’re tied to the model types, and those change from app to app. That said, I do have a couple of patterns that I follow for fixtures. First, I prefer to make the fixtures static methods on the data type they’re creating instances of. In my experience, this scales a bit better than having one fixtures namespace or type that all fixtures are created from. Second, while I have the fixture methods take the model’s properties as parameters, I do prefer to default those parameters to sane values. I have found that reduces test maintenance when I need to add a property to a model type.

To provide a concrete example, here’s the user fixture used in example test above:


import Foundation
import Vapor
@testable import App

extension User {
    static func user(email: String = "bob.jimbob@example.com", on connection: DatabaseConnectable) -> User {
        let user = try! User(email: email)
        return try! user.create(on: connection).wait()
    }
}

There’s not much here, but I find even simple fixtures like this save time and boilerplate. The fixture creates a User instance, then saves it to the database. The most notable thing here is that the fixture waits on the database save to complete before returning. It does this by calling wait() on the promise.

The other thing that’s probably worth commenting on is my liberal use of force unwraps in the tests. Normally, I would not suffer a force unwrap to live (with a couple of exceptions). But I view tests as controlled environments that real users will never have to experience, and if something crashes it was probably a programmer error anyway.

TestApplication

Everything that I’ve written about so far has been pretty specific to my particular app. But there’s some testing infrastructure that I built that should be fairly portable. The next piece I want to talk about is the TestApplication.

The TestApplication represents the system under test. Its main function is to receive requests and send back responses. There’s some boilerplate code needed to make that ergonomic for tests. I chose to make TestApplication its own type (as opposed to an extension on Application) for a few reasons. First, I feel a bespoke type makes it clear which methods and properties are useful for tests, as opposed to production code. Second, having a separate type allows other testing properties and methods to have a convenient place to live.

With all that said, here’s the init and deinit of my TestApplication type:


import Foundation
import Vapor
import FluentPostgreSQL
@testable import App

class TestApplication {
    private let application: Application
    private let configuration: TestingConfiguration

    lazy var connection: PostgreSQLConnection = {
        return try! application.newConnection(to: .psql).wait()
    }()
    var container: Container {
        return application
    }

    init(arguments: [String] = CommandLine.arguments) throws {
        var env = Environment.testing
        env.arguments = arguments

        self.application = try App.app(env)
        self.configuration = try application.make(TestingConfiguration.self)
    }

    deinit {
        try! application.releaseCachedConnections()
        try! application.syncShutdownGracefully()
    }
}

The TestApplication type mainly wraps Vapor’s Application type. However, it also contains the TestingConfiguration, which is helpful for tests wanting to grab the API key, or fake services, or any other piece of configuration data. The TestApplication also exposes a database connection, lazily created, and a dependency injection container. These objects are needed to do anything interesting in a Vapor app.

The init creates a testing environment and sets the command line arguments on it. The command line argument functionality will be used later, when resetting the database between tests. But for now, init ensures the application will be stood up using the TestingConfiguration. After the application is created, a TestingConfiguration instance is pulled out and stored in a property.

I didn’t write a deinit method to start with. After I had written severals tests I started getting test failures because all the Postgres connections were being used, and after that no test could acquire a new one. So the deinit manually clears any cached connections (Vapor seems to keep a pool around), then waits synchronously until the Vapor app fully shuts down.

Making test requests

I’ve created my TestApplication now, and while it does some helpful things, I haven’t covered the main thing it does: send requests and receive responses. There were couple of pieces to implementing this. First, I needed to put together a method that can do basic send and receive. Then I added several methods to make it ergonomic to call from tests.

To start, I built the basic workhorse method:


class TestApplication {
  ...
  private func request<T: Content>(_ path: String, method: HTTPMethod, headers: HTTPHeaders, body: T?) throws -> TestResponse {
       let responder = try application.make(Responder.self)
       let httpRequest = HTTPRequest(method: method, url: URL(string: path)!, headers: headers)
       let request = Request(http: httpRequest, using: application)
       if let body = body {
           try request.content.encode(body)
       }
       return try TestResponse(response: responder.respond(to: request).wait())
  }
  ...
}

The first thing I’ll point out here is the method signature. It takes a path to the API to be called, the HTTP method to use, headers, and an optional body. The body can be any type as long as it conforms to Vapor’s Content protocol. The method can throw, and returns a TestResponse if it is successful. These parameters cover all the options that my tests care about.

The first thing request() does is create an object that conforms to the Vapor Responder protocol. This is the object that allows TestApplication to make requests and receive responses. Then request() creates the raw HTTPRequest using the parameters passed in, then the Request object based on that. The using parameter wants a dependency injection container, so I give it the application. If there is a non-nil body provided in the parameters, it is encoded into the request. Now that the request is fully created, it is given to the Responder which returns a promise to the response. Since this is for tests, I wait() on the response promise to be fulfilled before wrapping it in a TestResponse.

Although this method removes a lot of the boilerplate needed to make a request, I felt like it could be a bit cleaner. I figured that the HTTP method could be the method name instead of a parameter. So I added some methods that do that:


class TestApplication {
  ...
  func get<T: Content>(_ path: String, headers: HTTPHeaders = HTTPHeaders(), body: T?) throws -> TestResponse {
      return try request(path, method: .GET, headers: headers, body: body)
  }

  func put<T: Content>(_ path: String, headers: HTTPHeaders = HTTPHeaders(), body: T?) throws -> TestResponse {
      return try request(path, method: .PUT, headers: headers, body: body)
  }

  func post<T: Content>(_ path: String, headers: HTTPHeaders = HTTPHeaders(), body: T?) throws -> TestResponse {
      return try request(path, method: .POST, headers: headers, body: body)
  }

  func delete<T: Content>(_ path: String, headers: HTTPHeaders = HTTPHeaders(), body: T?) throws -> TestResponse {
      return try request(path, method: .DELETE, headers: headers, body: body)
  }
  ...
}

These methods also default the headers to an empty set of headers, in the cases where tests don’t care about them. These methods are pretty ergonomic, but there’s one more case I needed to handle.

Some of my APIs don’t send a body in the request. The workhorse request() method can handle that; the caller just passes in nil in that case. However, because body is a generic type, the compiler needs a type — any type — to compile the call. nil by itself won’t give the compiler enough information to infer the type. I needed an instance of an optional type set to nil to satisfy the type inference. I also wanted to avoid forcing the test from having to pass that instance in. If a body parameter wasn’t provided, the TestApplication methods should assume there isn’t one.

Fortunately this can be done easily, albeit with a bit more boilerplate:


class TestApplication {
  ...
  struct Empty: Content {
     static let instance: Empty? = nil
  }

  func get(_ path: String, headers: HTTPHeaders = HTTPHeaders()) throws -> TestResponse {
     return try get(path, headers: headers, body: Empty.instance)
  }

  func delete(_ path: String, headers: HTTPHeaders = HTTPHeaders()) throws -> TestResponse {
     return try delete(path, headers: headers, body: Empty.instance)
  }
  ...
}

I defined a placeholder type called Empty and conformed it to Vapor’s Content protocol. The protocol conformance was to satisfy request()‘s constraints on the body parameter. Then I declared an optional static instance of the type, and set it to nil. I could then use this static instance to represent an empty request body.

Now that I had the Empty.instance instance created, I used it to remove the body parameter. Here, I’ve only provided versions of the get and delete methods without a body, but the same technique works for put and post.

At this point I could now make requests like I showed in my example test. But how about validating the response I got back?

TestResponse

The main goal of TestResponse was to make it easy for tests to validate the response. I chose to wrap Vapor’s Response type instead of providing an extension on it for the same reason TestApplication wraps Application. The main things my tests want to do with a response is: verify the HTTP status, verify the body, and maybe verify a header or two.


import Foundation
import Vapor

struct TestResponse {
    private let response: Response

    init(response: Response) {
        self.response = response
    }

    var status: HTTPResponseStatus { return response.http.status }

    func content<T: Decodable>(decodeTo type: T.Type) throws -> T {
        return try response.content.decode(type).wait()
    }

    func header(_ name: HTTPHeaderName) -> String? {
        return response.http.headers.firstValue(name: name)
    }
}

TestResponse is simple. It exposes the HTTP status in status, provides a synchronous way to decode the body via the content() method, and looks up a given header with the header() method. For more complicated tests it might need to expose more information, but this works for all my tests.

Resetting the database

So far, I’ve been mainly concerned with sending requests, receiving responses, and validating them. But for tests to work, I needed to be able to reset the database between test runs. Otherwise tests would interfere with one another.

First, I needed to set up some Fluent commands in my configuration. These added command line arguments that can reset the database for me.


public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services, _ configuration: ConfigurationType) throws {
  ...
  var commandConfig = CommandConfig.default()
  commandConfig.useFluentCommands()
  services.register(commandConfig)
  ...
}

There’s not much to this. Toward the end of my configure() method, I created a default Vapor CommandConfig, told it to use Fluent commands, then registered it as a service.

Now that the Fluent commands were set up, I needed to add a couple of methods to TestApplication to have it reset the database.


class TestApplication {
  ...
  func run() throws {
     try application.asyncRun().wait()
  }

  static func reset() throws {
     try TestApplication(arguments: ["vapor", "revert", "--all", "-y"]).run()
     try TestApplication(arguments: ["vapor", "migrate", "-y"]).run()
  }
  ...
}

The run() method looks a bit odd by itself. It synchronously runs the application. Which turned out to be handy, if it has some commands to run from the command line.

The reset() static method provided those command line arguments. First, it creates an instance of the app that reverts the database, and runs it. Then, it it creates an instance of the app that runs all migrations.

That testing database

I’ve blissfully ignored that all this needs a real test database to talk to. I could have set up a local Postgres instance on my laptop (which I’ve done before), but that doesn’t scale well if I have multiple apps. So I’ve been using Docker to handle standing up a test database.

I already had Homebrew installed, so I used it to install Docker:


brew cask install docker

Then I started a Postgres instance running in Docker for the test suite to talk to with this command:


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

This names the instance myapp-postgres-test so I can refer to it later. The -e flag sets environment variables, and POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD values all match the values I set in my TestingConfiguration. The -p flag tells Docker to publish Postgres’s default port (5432) on port 5433, which is where my TestingConfiguration was pointing. The -d flag tells Docker to run the container in the background, and postgres argument is the Docker image to use.

At this point, I can run all tests in Xcode (menu: Product > Test) or run vapor test from the command line.

Sometimes during development I changed the database schema in way that my migrations didn’t catch. In these cases, I was lazy, and I just reset the Docker image:


docker stop myapp-postgres-test
docker rm myapp-postgres-test

This first stops the container, then removes it, leaving me free to re-execute the docker run command above to stand up a fresh Postgres database.

As a final testing infrastructure tip, I’ll start by observing that Swift on Linux doesn’t currently have reflection capabilities. That means test cases have to be manually given to the testing framework as a parameter. I find this is tedious and error prone, especially given how forgetful I am. Therefore, I let the Swift Package Manager autogenerate the necessary information for me:


swift test --generate-linuxmain

Unfortunately the generated code is static, so I have re-run the command anytime I add or remove a test case.

Also, I’m terrible at remembering all these Docker and Swift Package Manager commands, so I added them to my app’s README.md file.

Conclusion

With API servers, I find writing integration tests more helpful than unit tests. However, in my experience there’s some boilerplate code needed to make creating integration tests in Vapor comfortable. My approach is create a TestApplication class to model the system under test, and make to it ergonomic to send requests and synchronously receive responses. Likewise, I use a TestResponse type to make validating responses easier. I also leverage fixtures for seeding the test database, and a utility function for resetting the database between test runs. Finally, I make use of Docker to make it convenient to stand up and reset the Postgres testing database.