Upcoming and OnDemand Webinars View full list

Request Testing in Vapor, Part 1

Josh Justice

Vapor’s 2.0 release has dramatically streamlined its testing offerings. With Vapor 2.0, you can use well-established testing practices for server-side applications. Let’s see how we can set up request tests: tests that simulate the user accessing the app. You can follow along with these steps, or get the completed project from GitHub.

Create a New Vapor Project

We’ll start with the sample app generated by Vapor. First, install the Vapor Toolbox. Run vapor version and make sure the output shows Vapor Toolbox: 2.0.0 or a later version. If not, run brew upgrade vapor to upgrade it.

Generate a new Vapor app by entering the following commands in the Terminal:

  • vapor new nerdserver
  • cd nerdserver
  • vapor xcode -y

This will download your new app’s dependencies, create an Xcode project file, and open it in Xcode.

This default app includes a model (Post) and controller (PostController), and those will work fine for our testing. If you’d like to learn about creating your own models and controllers, check out Server-Side Swift with Vapor.

Attaching to a Persistent Database

The only problem with the generated app is that, by default, it uses an in-memory database that is cleared when the server is stopped, and most production web apps will need a persistent database. A persistent database also causes some challenges for testing, and we want to run across these challenges so we can see how to get past them.

Let’s hook our app up to PostgreSQL (also shortened to “Postgres”), a popular database for web apps. There are a few options for installing Postgres; if you’re on a Mac, Postgres.app is the easiest way to get started. Install Postgres one way or the other, start the database server, then run the command createdb nerdserver_development. This will create a database for our app to use.

Next we need to connect our Vapor app to Postgres. Add Vapor’s postgresql-provider package to your Package.swift:

     dependencies: [
         .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
-        .Package(url: "https://github.com/vapor/fluent-provider.git", majorVersion: 1)
+        .Package(url: "https://github.com/vapor/fluent-provider.git", majorVersion: 1),
+        .Package(url: "https://github.com/vapor/postgresql-provider.git", majorVersion: 2),
     ],

Run vapor xcode -y; this downloads the postgresql-provider package, regenerates your Xcode project file, and reopens it.

The postgresql-provider automatically looks for a Config/secrets/postgresql.json file to load database connection info. Under the Config/ folder, create a secrets/ folder, then create a postgresql.json file under it and add your connection info. Check the docs for your Postgres installation to find your default connection info; if you installed Postgres.app like I did, your info should be:

{
    "hostname": "127.0.0.1",
    "port": 5432,
    "user": "postgres",
    "password": "",
    "database": "nerdserver_development"
}

Now we need to update sources/App/Config+Setup.swift to load PostgreSQLProvider. Import the package:

 import FluentProvider
+import PostgreSQLProvider
 
 extension Config {

Then add the provider in setupProviders():

     /// Configure providers
     private func setupProviders() throws {
         try addProvider(FluentProvider.Provider.self)
+        try addProvider(PostgreSQLProvider.Provider.self)
     }

Finally, update config/fluent.json to tell Fluent to use Postgres instead of the in-memory database:

     "//": "Other drivers are available through Vapor providers",
     "//": "https://github.com/search?q=topic:vapor-provider+topic:database",
-    "driver": "memory",
+    "driver": "postgresql",
 
     "//": "Naming convention to use for creating foreign id keys,",

Choose the App target from Xcode’s target dropdown, then click Xcode’s Run button. Now you should be set to create posts in your Postgres database! Let’s give it a try using curl, a command-line program for sending HTTP requests:

# curl 
    --header 'Content-Type: application/json' 
    --data '{"content": "Hello, world!"}' 
    http://localhost:8080/posts
{"content":"Hello, world!","id":1}

# curl http://localhost:8080/posts
[{"content":"Hello, world!","id":1}]

(You can learn more about using cURL to test RESTful web services if you aren’t already familiar with it.)

Writing a Request Test

Now that our app is set up with Postgres, we’re ready to create our request test. A request test will send simulated HTTP requests to our Droplet and check the responses it returns.

An alternative approach is controller testing, which involves directly calling methods on your controller instance; this is demonstrated in the PostControllerTests class that Vapor generates. The benefit of the request test approach is that requests tests are as “end-to-end” as possible, confirming that all the pieces of your app are working together as expected. For example, a request test can confirm that your controller is accessible at a given route and that it is protected by authentication middleware, but a controller test cannot.

There’s an even more practical reason we don’t want to use PostControllerTests right now: it was written to work against an in-memory database, and it crashes now that we’re using a persistent database instead. To prevent this from causing problems with your test runs, delete PostControllerTests.swift. Make sure to keep the Utilities.swift file that’s in the same group; we’ll need it.

Create a PostRequestTests.swift file inside your Tests/AppTests group. In Xcode’s file-add dialog, it will probably default to an incorrect target like libc: uncheck that target and check the AppTests target at the bottom of the list.

Replace the contents of the file with the following:

import HTTP
import Vapor
import XCTest
import Testing

class PostRequestTests: XCTestCase {
    
}

We’ll need an instance of our Droplet in each test method we create, so let’s make a property to hold it. We’ll initialize it using the Droplet.testable() method Vapor provides for us:

 class PostRequestTests: TestCase {
+    let droplet = try! Droplet.testable()
 }

We also need to set up Vapor’s request test helpers to report failures using XCTFail:

 class PostRequestTests: TestCase {
     let droplet = try! Droplet.testable()
+ 
+    override func setUp() {
+        super.setUp()
+        Testing.onFail = XCTFail
+    }
 }

Now let’s create our test method. We’ll start by setting up the data our test needs:

    func testCreate() throws {
        let testContent = "test content"
        let requestBody = try Body(JSON(node: ["content": testContent]))
        
        let request = Request(method: .post,
                              uri: "/posts",
                              headers: ["Content-Type": "application/json"],
                              body: requestBody)
    }

We define a test content string, then format a request body with the JSON the server expects. Then we create a Request to POST that data to the /posts route.

Next we need to actually send the request and get a response:

         let request = Request(method: .post,
                               uri: "/posts",
                               headers: ["Content-Type": "application/json"],
                               body: requestBody)
+        try droplet.testResponse(to: request)
+            .assertStatus(is: .ok)
+            .assertJSON("id", passes: { json in json.int != nil })
+            .assertJSON("content", equals: testContent)
}

We perform a few assertions against the response:

  • We check the HTTP status of the response to ensure it’s .ok, not a problem like .notFound or .internalServerError.
  • We confirm that the response body is JSON containing an integer ID and the content string we sent in.

Switch to the Test tab, Control-click on AppTests, and choose Enable “AppTests”. Now, run your test from Product > Run or by pressing Command-U. It should pass!

That’s great, but we want to make sure the test will really fail if we get something wrong. Open PostController and find the create(request:) method. Replace return post with return JSON([:]). This will return an empty JSON object instead of the created post. Run your test again. You should test failures for the assertions looking for the id and the content. Switch your code back to return post, rerun the test, and it should pass again.

What’s Next?

We’ve got a working request test, which is great! If we add additional validations or business logic in the future, this test will make it easy for us to confirm we haven’t broken the existing behavior. It will also help if we need to refactor the code, making changes to it to handle new requirements.

But there’s one thing wrong: run your app, then run curl http://localhost:8080/posts to list the posts again. You’ll see your “test content” post in there! The data from your tests is showing up in your development environment. That’s not too big a problem now, but, once you have more tests, your test posts will start to overwhelm your dev data.

In my next post, we’ll look at how to set up a separate testing database to keep that data separate, as well as some other techniques to make it easier to work with databases in our tests.

In the meantime, if you need to create a Swift on the Server app, Big Nerd Ranch would love to talk with you. We can offer a unique combination of cross-platform backend experience and deep Swift knowledge. Get in touch with us for more information on what we can do for you!

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project