Voyage au centre d’iOS

Fastlane as Continuous Integration

Today we will see the continuous integration for our iOS application. Firstly, what is it? It allows us to integrate, test, and find bugs during the whole process of the development. Usually we build, test and report at every pull/merge requests. To not do it manually, we use some server like Jenkins, Gitlab-ci or Bitrise.

How does it work? A simple CI is a simple script that :

  • fetch dependencies
  • build the app
  • run the automated tests
  • report linters, code coverage

To do this in iOS, we use Fastlane. The final fastfile is available at the end of the article

Fastlane

Fastlane is a powerful tool written in ruby. It automates all the not-so-cool things like the signing of your app or the upload to the store.

For our purpose we will use it to call our CI with a simple command. It will also allow us to run our CI in any platform, like our computer or our CI server.

Install

The first thing to do is to install Fastlane. We use Bundler to do so. Bundler will manage our versions for us, and allow us to have exactly the same version of Fastlane for every platform.

Create a Gemfile at the root of our project and add this in it

source "https://rubygems.org"

gem "fastlane"
gem "cocoapods"
gem "slather"

As we can see, we also add cocoapods and slather right here, for exactly the same reason as fastlane. We want to have the same tools with the same version for every development computer and every server.

To install them, we just have to execute

bundle install

We can now initialize our fastlane project.

Run in your terminal, in the root of your project

bundle exec fastlane init

We precede every command implying cocoapods, fastlane or slather by bundle exec to ensure it uses the same version of the tools.

When Fastlane ask you about what you want, set the manual setup. It will create a blank fastfile.

The fastfile is where we configure our actions using lanes. A lane is a simple suite of actions, and an action is a simple command like cocoapods or sh.

If we open our fastfile, (in Fastlane/fastfile) we can see

default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :custom_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end
end

As we can see, fastlane works using some lanes.

To create a lane we just have to add

  desc "My lane description"
  lane :new_lane do
  end

Where desc is the description, new_lane the name of our lane and the script will be between the do and the end.

Dependencies

Now that we have fastlane, it’s time to write our first step. Every step will be a lane, and in the end we will write a lane CI that launch all of them. We do that so we can run every action independently if needed (e.g.: to debug).

The first step of our CI will be to fetch the dependencies of our application. In our case we use Cocoapods, but it’s the same thing for Carthage.

In this example, I assume that you already have a project with cocoapods and a proper podfile.

Add in your fastfile

desc "Cocoapods"
lane :dependencies do
  cocoapods
end

If you have any troubles, you can find all the options in this doc.

Build

Next step, we will build our application.

To do so, add in your fastfile

  desc "Build"
  lane :build do
    build_ios_app(
      scheme: "MyScheme",
      export_method: "development"
    )
  end

Do not forget to change MyScheme with your current scheme. We build there in development not to have to worry about signing with certificates and provisioning profiles.

We can use this lane if we don’t have any test and don’t want to add some, which is a bad idea, but it’s not the subject of this post.

Test

For this step you need to have a test target. If not you can add one using file->new->Target->Unit Testing Bundle

Add to your fastfile

  desc "Tests"
  lane :test do
    run_tests(
      scheme: "MyScheme",
      output_types: "junit",
      output_directory: "build",
      output_files: "test_result.xml"    
    )
  end

Do not forget to change MyScheme with your current scheme. We set the output_types to junit and set a specific path for the output_files and output_directory to use them in the slather lane.

If we use this lane, we won’t need the build lane. The run_test will build our application so it would be redundant to use build then test lanes.

SwiftLint

Now that our CI does all the dependencies, build, test stuffs, it’s time to add some cool reports to show in our merge/pull requests.

The first one that is great to add, is a linter. We use SwiftLint, it’s the most popular one. To install it, we use cocoapods, to allow us to have the same version everywhere.

Add to your fastfile

  desc "Report Linters from Pods"
  lane :lint do
    swiftlint(
        executable: "Pods/SwiftLint/swiftlint",
        output_file: "build/swiftlint.json",
        ignore_exit_status: true,
        reporter: "json"
    )
  end

Every output will be in the build folder. It allows us to ignore it directly in the .gitignore. As said before, we installed it from cocoapods, so we have to set the executable path.

We set the ignore_exit_status to true, to allow our CI to continue even with a critical error, so that we have all the reports in our hands.

Slather

Another report to have is the code coverage. The lane run_test already gives us one, but we want a prettier one, so we will use Slather.

To enable slather to gather the code coverage, go to your scheme->Edit Scheme->test->Options and enable Gather coverage for and set some targets with only your app target.

The second step is to add a .slather.yml to set some information

coverage_service: cobertura_xml
workspace: MyWorkspace.xcworkspace
xcodeproj: MyProject.xcodeproj
scheme: MyScheme
output_directory: "build/slather-report"
ignore:
  - Pods/*
  - MyProjectTests/*
  - Generated/*

Do not forget to update MyWorkspace, MyProject, MyScheme and MyProjectTests with yours. Here, we set cobertura_xml as a service to allow an other tool to manage it. You can also add a coveralls export to have a pretty view of our coverage. We can set html directly if we want a simple html view that can be hosted in gitlab page for example.

Here is an example of the html export

slather html example

Final fastfile

Now we have all our lanes ready to be called. The last step is to add a simple lane we called CI.

  desc "CI"
  lane :ci do
    dependencies
    test
    codecoverage
    lint
  end

To test all our CI, just run

bundle exec fastlane ci

We can see all the reports in the build folder, and have a direct return in the output of the command.

Our fasfile should look like that


default_platform(:ios)

platform :ios do
  desc "Cocoapods"
  lane :dependencies do
    cocoapods
  end

  desc "Build"
  lane :build do
    build_ios_app(
      scheme: "MyScheme",
      export_method: "development"
    )
  end

  desc "Tests"
  lane :test do
    run_tests(
      scheme: "MyScheme",
      output_types: "junit",
      output_directory: "build",
      output_files: "test_result.xml"    
    )
  end

  desc "Report Linters from Pods"
  lane :lint do
    swiftlint(
        executable: "Pods/SwiftLint/swiftlint",
        output_file: "build/swiftlint.json",
        ignore_exit_status: true,
        reporter: "json"
    )    
  end

  desc "Slather"
  lane :codecoverage do
    slather(
      use_bundle_exec: "true",
    )
  end

  desc "CI"
  lane :ci do
    dependencies
    test
    codecoverage
    lint
  end
end

What’s next ?

This article shows you one of the simplest CI using Fastlane.

Obviously we can add some notifications using slack, mail. We can add some reports to be written directly as a comment in our merge/pull request like SwiftLint. We can add pre-check for our merge/pull requests using Danger.

Now that we have this CI, feel free to go to the fastlane doc and add new actions.

The next step will be to use it in our CI server.