Articles, podcasts and news about Swift development, by John Sundell.

Kicking off 2021 with solid continuous integration using Bitrise

Published on 04 Jan 2021

The start of a new year is often an excellent time to revisit old assumptions, to clean up some long-standing technical debt, or to improve the overall suite of workflows and automation that’s used within a given project.

One thing that intersects more or less all of those areas is continuous integration, which — as a quick recap — is the act of continuously merging new changes into a project’s main branch (rather than using long-lived feature branches), and to always run a certain set of automated tests on those changes in order to ensure that the project continues to build and run as expected.

For long-time Swift by Sundell readers, it should come as no surprise that my favorite tool for continuous integration is Bitrise, which have also helped me keep Swift by Sundell up and running all throughout the past two weeks of holidays. So to round off their latest sponsorship, I thought I’d share a few tips on how I like to configure my various projects on Bitrise. Perhaps some of these tips will help you start 2021 by adding some really solid continuous integration to your project.

The basics go a long way

One of my favorite things about Bitrise is just how easy it is to configure. For many of my projects, their automatic setup process (which detects what kind of project you’re working on and configures a CI pipeline accordingly) turned out to be everything that I actually needed. Because although there’s a nearly endless amount of verification that we could perform as part of our CI process, sometimes all that we really need to do is to compile the project and run all of its tests.

So if you haven’t yet used Bitrise, my suggestion would be to simply try it by setting it up with that automatic configurator, to see how it could help you improve your project’s development workflow.

Splitting up a larger project into multiple workflows

As a project grows, it might be a good idea to split its CI process up into multiple, separate workflows. Not only can that often improve the overall speed of getting a given Pull Request or commit green-lit by the CI system, it can also make debugging failures easier, as each workflow will end up having a much more narrow scope.

For example, let’s say that we’re working on an app that ships on both iOS and macOS. While we could build and test both of those two variants within the same Bitrise workflow, it’ll most likely be faster to use two separate ones. The good news is that Bitrise supports using the same project for multiple workflows, all that we have to do is to configure each one as a new app (but using the same repository). For example, Publish (which is the static site generator used to build all of Swift by Sundell) has two separate Bitrise workflows, one for macOS and one for Linux, that can both run in parallel.

There are also other ways that CI workflows could be split up as well. You could, for example, run all of a project’s UI tests within a separate workflow (since those tend to take the longest to run), or run the tests for any internal frameworks separately, or use separate workflows to test your app using multiple Xcode/SDK versions.

Of course, starting with just a single workflow is probably the best approach, as parallelization always tends to come with a certain amount of complexity. In this case, we might also need to be careful to keep each of our workflow configurations in sync, which could be done using a Git-hosted workflow YML file.

Handling testing timeouts and flakiness

One of the most commonly faced challenges when adopting a more CI-centric workflow is flakiness, which is when a given set of tests and verifications seem to either succeed or fail at random.

Even though Bitrise ships with a fair amount of built-in functionality that helps reduce the potential for flakiness (such as automatic retries for UI tests), at the end of the day, the overall, long-term stability of our tests will always come down to how they were actually written.

When it comes to unit and UI tests in particular, one of my top tips on how to avoid flakiness is to always use generous timeouts when calling APIs like waitForExpectations and waitForExistence, and to avoid using blocking waiting techniques, such as sleep.

If we use proper waiting APIs when writing asynchronous tests, the test runner will only pause for the whole timeout interval when a given condition or expectation wasn’t fulfilled — so using a larger timeout won’t slow down the overall execution of our tests, but will give our operations some extra time to finish when running in more resource-constrained environments, which CI servers typically are.

To learn more about testing timeouts and stability, check out my guest article “Making Xcode UI tests faster and more stable” on the Bitrise blog and “Unit testing asynchronous Swift code” right here on Swift by Sundell.

Conclusion

Regardless if you’re completely new to the concept of continuous integration, or if you already have an existing setup, now might be a great time to prepare each of your projects for the upcoming year by ensuring that you have some really solid verifications in place — and if you’re looking for a platform to implement your CI workflows on, then I really recommend checking out Bitrise. It’s free to get started, and it’s the CI tool that I personally use for all of my new projects.