Server-Side Swift: Testing on Linux

In this tutorial, you’ll test your server-side Swift apps on Linux, learning the differences between testing on macOS and Linux, and how to use Docker and Docker Compose. By Christian Weinberger.

Leave a rating/review
Download materials
Save for later
Share
Update note: Christian Weinberger updated this tutorial for Swift 5.3. Tim Condon wrote the original.

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 tutorial, you’ll learn:

Why Test on Linux?

Getting Started

Testing is a key part of any app development and deployment. You’ll find this helpful tutorial that covers how to test Vapor apps and how to write tests for server-side Swift projects.

Testing on Linux is important as well. You’d usually deploy your app to an operating system that is different from the one you used for development. However, in this case it’s vital that you test your app in the same environment that you deploy in. Why is it important?

Foundation on Linux isn’t the same as Foundation on macOS. On macOS, it still uses the Objective-C framework, which has been thoroughly tested over the years. Linux uses the pure Swift Foundation framework, which isn’t as robust. The implementation status list shows that many features remain unimplemented on Linux. If you use these features, your app may crash. Although the situation improves constantly, you must still ensure everything works as expected on Linux.

Start by downloading the materials for this tutorial, using the Download Materials button at the top or bottom of this tutorial. The starter project contains a Vapor app with some tests. Open Package.swift. You’ll see a test target defined in the targets array:

  • How to test your server-side Swift apps on Linux.
  • What the differences are between testing on macOS and Linux.
  • How to use Docker and Docker Compose to easily test your apps locally on Linux.
  • Note: This tutorial requires Docker. Check out Docker on macOS: Getting Started to learn the basics of and how to install Docker.
Note: This tutorial requires Docker. Check out Docker on macOS: Getting Started to learn the basics of and how to install Docker.
.testTarget(name: "AppTests", dependencies: [
  .target(name: "App"),
  .product(name: "XCTVapor", package: "vapor"),
])

This defines a testTarget type with a dependency on App and XCTVapor (providing Vapor test helpers on top of XCTest). Tests must live in the Tests/ directory. In this case, that’s Tests/AppTests.

Checking the Tests

The tests connect to a PostgreSQL database, with the configuration defined in configure.swift in Sources/App. The tests are expecting a database on port 5432 called vapor-test, which accepts a username of vapor_username and a password of vapor_password. This helps avoid conflicting Docker containers when running the app locally.

In Terminal, enter the following:

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

This:

  • Runs a new container named postgres-test.
  • Specifies the database name, username and password through environment variables.
  • Allows apps to connect to the Postgres server by mapping the external port 5433 to the container’s port 5432.
  • Runs the server in the background as a daemon.
  • Uses the Docker image named postgres for this container. If the image isn’t present on your machine, Docker automatically downloads it.

Then run the tests. In Terminal, make sure you’re in the starter directory, then enter the following:

swift test

This pulls down the required dependencies, compiles the project and runs the tests. You’ll see all the tests complete successfully:

Result of running `swift test` in terminal

To clean up, in Terminal, run the following commands:

# 1
docker stop postgres-test
# 2
docker rm postgres-test

These commands:

  1. Stop the postgres-test container in which you ran your tests.
  2. Delete the postgres-test container now that it’s stopped.

Running Tests on Linux

Test Discovery on Linux

On macOS, swift test still uses Objective-C to find the tests to run. On Linux, there’s no Objective-C runtime to discover your XCTestCases.
However, since Swift 5.1 you can run Swift tests on Linux with a flag to automatically discover tests. And because your project uses Swift 5.3, you can use this new flag:

swift test --enable-test-discovery

This command can be run on Linux machines and doesn’t require you to create or generate a LinuxMain.swift class before it can run tests on Linux.

It will discover and run your tests. Because this flag is available on Linux, you can use it in your continuous integration flow as well.

Note: Before Swift 5.1, you had to write or generate LinuxMain.swift and XCTestManifests.swift classes to tell Swift which tests to run on a Linux machine. In case you are running into issues with automatic test discovery (e.g., SR-11951), you can fall back to generating these files using swift test --generate-linuxmain. Because this command must be run on macOS, you should run it and commit LinuxMain.swift to your remote Git repository to have it available for your Continuous Integration tests on Linux.

Early feedback is always valuable in software development, and running tests on Linux is no exception. Using a Continuous Integration system to automatically test on Linux is vital, but what happens if you want to test on Linux on your Mac? You’ll do that now with Docker.

Generating a Dockerfile

Well, you’ve already used Linux for the PostgreSQL test database using Docker! So you can also use Docker to run your tests in a Linux environment. In the top level project starter directory, create a new file called Dockerfile (with no extension). Open the file in a text editor and add the following:

# 1
FROM swift:5.3
# 2
WORKDIR /package
# 3
COPY . ./
# 4
CMD ["swift", "test", "--enable-test-discovery"]

The Dockerfile:

  1. Uses the official Swift 5.3 image.
  2. Sets the working directory to /package.
  3. Copies the contents of the current directory into /package in the container.
  4. Fetches the dependencies and cleans up the project’s build artifacts.
  5. Sets the default command to swift test --enable-test-discovery. This will also fetch dependencies and clean up the project’s build artifacts.

Using Docker Compose

As described earlier, the tests need a PostgreSQL database to run. By default, Docker containers can’t see each other. However, Docker has a tool, Docker Compose, designed to link different containers for testing and running apps. Create a new file called docker-compose.yml in the top level starter project directory. Open the file in an editor and add the following:

# 1
version: '3'
# 2
services:
  # 3
  til-app:
    # 4
    depends_on:
      - postgres
    # 5
    build: .
    # 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

This code:

  1. Specifies the Docker Compose version.
  2. Defines the services for this app.
  3. Defines a service for the TIL app.
  4. Sets a dependency on the Postgres container, so Docker Compose starts the Postgres container first.
  5. Builds the Dockerfile in the current directory — the Dockerfile you just created.
  6. Injects the DATABASE_HOST and DATABASE_PORT environment variable. Docker Compose has an internal DNS resolver. This
    allows the til-app container to connect to the postgres container with the hostname postgres and port 5432.
  7. Defines a service for the Postgres container.
  8. Uses the standard Postgres image.
  9. Sets the same environment variables as were used at the start of the tutorial for the test database.

To test your app on Linux, in Terminal, type the following:

# 1
docker-compose build
# 2
docker-compose up --abort-on-container-exit

This:

  1. Builds the different Docker containers.
  2. Spins up the different containers and run the tests. --abort-on-container-exit tells Docker Compose to stop the postgres container when the til-app container stops. The postgres container used for this test is different from, and doesn’t conflict with, the one you ran earlier for testing on macOS.

When the tests finish running, you’ll see the output in Terminal with all tests passing:

Result of running the tests in docker using `docker-compose`

Note: You could also run the tests on a real Linux machine. You can move the whole starter folder to the Linux machine. Remember to remove the .build folder so dependencies are recompiled correctly when you run swift test --enable-test-discovery. Your Linux machine will need PostgreSQL (as configured in the Docker example above) and the following packages installed: libssl-dev, libz-dev and pkg-config, which you can install via sudo apt install libssl-dev libz-dev pkg-config.

To clean up, in Terminal, run the following commands from the root folder of your project:

# 1
docker-compose down
# 2
docker-compose rm

These commands:

  1. Stop all containers and networks created by docker-compose.
  2. Delete the previously created containers now that they are stopped.