Vapor 3 Series III — Testing

Photo by Zany Jadraque on Unsplash

In our previous article, we implemented two ways to authenticate users: HTTP basic authentication and bearer token authorization. As a result, our application only accepts requests from authenticated users. However, we should write some unit tests for our endpoints, even though our application is quite simple. Testing is an important part of software development, and writing unit tests allows us to develop and evolve our application quickly. In order to develop new feature quickly, we want to ensure that the existing features don’t break, and having a thorough test suite lets us verify everything still works as we change our codebase. In this article, I am going to demonstrate how to write unit tests for our CRUD endpoints and how to run the tests on Linux machine with Docker.

Please notice that this article will base on the previous implementation.

Preparation

One benefit to write our application in Swift is that we are able to run tests with the lovely Xcode. However, in order to run tests with Xcode, we have to generate the test target beforehand. Please open Package.swift and replace the line .target(name: "Run", dependencies: ["App"]) with the following lines.

This tells Swift package manager, which is used by Vapor’s toolbox, to generate AppTests target for our Xcode project. Moreover, create AppTests folder and corresponding files with the following commands in Terminal as usual.

In order to make our tests concise, we are going to write some helper methods of Application as well as our Usermodel, so we create Application+Testable.swift and User+Testable.swift respectively. Besides, the unit tests for our endpoints will be located in UserTests.swift. Then, regenerate the Xcode project file with vapor xcode -ycommand. Finally, please open the project with Xcode to verify that the test target appears and the project structure looks like the image below.

Let’s start with writing the helper methods of Application. Please open Application+Testable.swift and add the following code.

Here, we import the necessary dependencies and create testable method which allows us to create a testable instance of Application. Again, within Application+Testable.swift, append the following lines.

We create sendRequest method which sends a request to a path and returns a Reponse. If the request requires authentication, the admin user is used to log in and obtain the token. Furthermore, EmptyBody type is used when there's no body to send in a request, because we cannot use nil for a generic type directly.

Next, open User+Testable.swift and add the following lines.

For our User model, not only do we import the necessary dependencies, but write a helper method to create a Userinstance and save it into the database as well.

Test Endpoints

After finishing our helper methods of Application and User model, we can focus on writing unit tests for our endpoints. First of all, open UserTests.swift and add the following code.

This generates an instance of Application for testing and creates a connection to the database before each test begins. In addition, it also closes the connection after each test ends.

Secondly, please append the following function at the bottom of UserTests class.

This function saves a new instance of our User model into the database via the endpoint POST /api/users/, and verifies the result with assertions. Furthermore, it fetch all of the User instances via the endpoint GET /api/users and verifies the result as well. Before running this test case, it's necessary to switch to Testing-Package scheme as the following image.

Now, we are able to run the test case and it should pass!

Then, we can write the test cases for the endpoints used for retrieving our User model. Inside UserTests.swift, append the following functions below testUserCanBeSaved function.

Basically, what these two test cases do is saving an instance of our User model into the database, and then trying to retrieve the instance via our endpoints GET /api/users/:id and GET /api/users respectively.

Finally, let’s add tests for the remaining endpoints PUT /api/users/:id and DELETE /api/users/:id. Please append the following functions under testAllUsersCanBeRetrieved function.

Similarly, we save an instance of our User model into the database, and then try to update and delete the instance via endpoints respectively. At this point, we cover all of our endpoints with unit tests, and we can run the test with Xcode to verify everything works fine.

Run Tests on Linux

When we deploy our application to a cloud service, we are deploying to an operating system different from the one used for development. Therefore, it’s important that we test our application on the same environment that we are going to deploy it on, which is likely a Linux environment. However, Foundation on Linux isn’t the same as Foundation on macOS, because Linux uses the pure Swift Foundation but macOS uses Objective-C counterpart instead. Running tests on Linux requires us to do things differently from running them on macOS, since there is no Objective-C runtime which determines the test methods provided by our XCTestCase. On Linux, we can declare test cases in LinuxMain.swift in the Tests folder, and this file is NOT part of our Xcode project. Open LinuxMain.swift with any text editor and add the following lines.

These lines provide an array for each XCTestCase, and allTests property contains a list of tuples consisting of the name of the test case. Switch back to UserTests.swift, and append allTests property below testUserCanBeDeletedfunction.

When we run tests on Linux, the test executable uses this array to determine which tests to run.

As an iOS developer, neither do I possess any Linux machine, nor want to run test on any cloud service at this stage. It turns out we are able to use Docker to run our tests in a Linux environment. Please follow this instruction to install Docker on your macOS. After installation, open Terminal to create a new file called Dockerfile in the project directory with command touch Dockerfile, and add the following lines into the file.

Basically, we declare the demand of Swift 4.1 image, the working directory, where the content is, fetching dependencies, and default command to run tests.

Although there is no external database or services used in our application, it’s still convenient to test our application with Docker Compose. Again, open Terminal to create a new file called docker-compose.yml in the project directory, and add the following lines into the file.

Here, we specify the version of Docker Compose, and define a service for our application. The last step is typing the following commands in Terminal to run the tests.

When the tests finish running, we will see the output in Terminal like the following image.

Conclusion

Here is the entire project.

Let’s recap this testing journey. First of all, we generate new the test target and corresponding files. Besides, we write the helper methods of Application and our User model, in order to make our test cases concise and readable. Secondly, we write the test cases for each endpoints with the helper methods, and run the tests with Xcode. Finally, we run the test cases on a Linux environment with Docker. Testing is a vital part of software development, and it's beneficial to write unit tests and automate them as much as possible.