Teabyte

Mobile app development for iOS and Swift

SPM tests on GitHub Actions

2020-12-13

In the past weeks I was creating more and more small Swift libraries to extract common utilities I use in my day to day work into separate modules. For this purpose I was using the Swift Package Manager (SPM) as it seems it will be the primary dependency manager to be used in Swift based projects in the future.

In this post I want to elaborate how to use SPM to create a standalone library and use GitHub actions to run the unit tests defined by your package.

Create a library

To create a library the first thing to do is to run the spm command line tool to create one.

mkdir MyAwesomeLibrary
cd MyAwesomeLibrary
swift package init --type library

This command creates the following base structure for a Swift library.

.
├── Package.swift
├── README.md
├── Sources
│   └── MyAwesomeLibrary
│       └── MyAwesomeLibrary.swift
└── Tests
    ├── LinuxMain.swift
    └── MyAwesomeLibraryTests
        ├── MyAwesomeLibraryTests.swift
        └── XCTestManifests.swift

The most important parts of this structure are the Package.swift file and the Sources and Tests directories. As the name already suggests, Sources and Tests contain your sources and test files respectively.

Package.swift

For now let us focus on the Package.swift. This file is a written manifest of you SPM package. It describes what your package contains and what it produces as its output.

Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
 
import PackageDescription
 
let package = Package(
    name: "MyAwesomeLibrary",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "MyAwesomeLibrary",
            targets: ["MyAwesomeLibrary"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "MyAwesomeLibrary",
            dependencies: []),
        .testTarget(
            name: "MyAwesomeLibraryTests",
            dependencies: ["MyAwesomeLibrary"]),
    ]
)
 

The first line sets the syntax version in which the manifest file is written in. This is important if you package has to work with older version of the SPM command line tools. The complete content inside the file is written in Swift, which makes it easy for Swift developers to structure and write such a file.

Within the products array developers can declare which executables and libraries their package is producing and make them visible in order to be consumed by other libraries or executables. In this small introduction we will just have one simple library which includes a single target.

The dependencies array declares other packages that the package we are creating is depending on. This is a package global description. In other words you can declare your dependencies here and import them throughout every target you define in your package.

targets declare the different targets that live within your package. They can depend on other targets of your package or on any of the external dependencies declared in the dependencies array.

If you want to have a look at a more sophisticated Package.swift file, you can have a look into my libraries/executables I created, e.g:

Please keep in mind that these files can change from time to time since they are subject to change due to undergoing development.

Run SPM Tests

To run the tests defined the manifest file the only thing we need to run is:

swift test --enable-code-coverage

Please note that I have added the --enable-code-coverage flag which generates a .json file. This file contains information about the code coverage of the tests. This is useful if you want to use some tool to visualize the code coverage of your tests.

Integrate SPM test into GitHub Actions

After looking at the SPM manifest file and having the knowledge of how to run tests for our small library, we can now create a workflow file for GitHub actions to execute the tests.

A workflow file is a description, written in the .yml format, of what GitHub Actions should run we certain events happen. The file must be placed under .github/workflows/, in your projects directory, in order for GitHub to find and parse it.

For our small example the file could look like this:

workflow.yaml
name: CI
 
on:
  # Trigger the workflow on pull requests on main and develop branch
  pull_request:
    branches:
      - main
      - develop
 
jobs:
  tests:
    name: Unit-Tests
    runs-on: macOS-latest
    steps:
        - name: Checkout repository
        uses: actions/checkout@v1
        - name: Run Unit Tests
        run: |
          swift test --enable-code-coverage
        - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          file: ./.build/debug/codecov/MyAwesomeLibrary.json
          flags: unittests
          name: codecov-umbrella
          fail_ci_if_error: true

The first thing to do is to define what should trigger the jobs. This is done via the on section. In this example the jobs section is run every time when a pull request is made to the main or develop branch.

The jobs section consists of one or more job that each defines a name, an environment in which the job runs and several smaller steps that are executed during the job. In this small example we have a single step that executes our unit tests. It has the name "Unit-Tests" and runs on the latest macOS version. You can also define a specific macOS version or even a linux distribution as the environment. A list of all available environments and their installed software can be found on the documentation page of GitHub.

Next, the different steps that are executed during the job are defined. A job can either reference an already existing step hosted on GitHub or be a shell script that is run when executed. When a external hosted step is referenced, the uses keyword is used. The run keyword is used when we want to execute a simple shell script as part of a step. There are many more keywords that can be used here. I would definitely recommend having a look into the syntax definitions for GitHub Actions to see which keywords are available.

In the example file above we can see types of steps:

External hosted:

  • actions/checkout@v1 to clone our repository to the environment
  • codecov/codecov-action@v1 action to upload our code coverage file to codecov
    • This step utilizes a secret API key which can be entered in your repositories settings: "Secrets -> Actions". The values entered there are encrypted and can be safely used within your steps without leaking them to anyone.

Custom ones:

  • Run Unit Tests which simply executes the shell script as we saw earlier when running the unit tests on our local machine

The only thing left to do now is to commit the workflow file to the repository. GitHub will then automatically verify that your file is syntactically right and afterwards run your defined workflow when the on triggers are matched. You can see an example in some of my libraries, e.g. in LoggingKit.

Conclusion

In this article we saw how we can create a Swift library with the help of the Swift Package Manager. Furthermore we integrated GitHub Actions in order to run the unit tests defined in the Package.swift file to verify that the code we want to merge to the develop and main branch is working correctly.

If you have suggestions or question don't hesitate to reach out to me !

See you around and stay healthy 👋