Flutter CI/CD with Codemagic, Sylph & AWS Device Farm

Oct 10, 2019

This article was originally published by Nathan Glover in DevOpstar.com

A common problem with developing for mobile is the sheer variety of device options that have to be tested for when designing UI. Traditionally teams might have had a set of different mobile devices that could be used to verify a consistent experience however without automation this QA process is easy to ignore.

In this post we’re going to look at a recent feature that Codemagic has begun to offer that allows automated integration testing of mobile devices to be baked into CI/CD pipelines. The feature is built on-top of Sylph which is a Flutter specific toolchain for orchestrating AWS Device Farm.

Requirements

The complete code for this post is available at here

Create Flutter Project

We’ll start off by creating a new Flutter project that can be used for this demo. I’ve used VSCode to perform this step. We’ll be setting up just enough in order to build for Android & iOS.

VSCode Create Flutter Project
VSCode Create Flutter Project

NOTE: If you are performing this step manually, run the following commands instead:

mkdir device_farm_demo && cd device_farm_demo
flutter create --ios-language swift --android-language kotlin

Run Flutter Project

Before doing anything, we should first check to ensure that Flutter builds and runs for each device type locally. Follow the steps below to run the application for each device type.

You can list the available emulators by running the following command:

flutter emulators

iOS Run

NOTE: You will first need to setup Xcode and create a new Simulator by following the offical guide

If you are on MacOS you can test locally using the simulator by running the following:

open -a Simulator
flutter run

Android Run

NOTE: You will first need to setup Android Studio and a Virtual device by following the offical guide

Open up a virtual device and run with the following command:

flutter run

Push Flutter Project

We’re making use of Codemagic CI/CD for the automated integration and deployment of our Flutter project. Because of this we’ll have to push our repository to a source control provider like GitHub that will be monitored and acted on when we make changes to the repository.

Create a new repository from the following page

GitHub create repository
GitHub create repository

Next push the local source code up to this new repository by running the following (ensuring to substitute in your own details).

git init
git remote add origin https://github.com/t04glovern/device_farm_demo.git
git add .
git commit -m "init"
git push -u origin master

Codemagic Setup

Before diving into device farm we should also ensure that the current Flutter app builds okay on Codemagic. Once this is confirmed we’ll be able to iterate faster while we’re learning. You can access Codemagic at codemagic.io/apps.

Sign in with your account (preferably GitHub since we’ve already pushed our project there). Once signed in import the new repository as a Codemagic project.

Codemagic Import Device farm project
Codemagic Import Device farm project

You will be prompted to select a branch, simply leave it on master and click Start new build.

Codemagic Import Device farm project build configuration
Codemagic Import Device farm project build configuration

The build could take up to 5 minutes to complete as it builds for both iOS and Android by default. Once successful you’ll see a green tick and we can safely move on.

Codemagic Sample build task complete
Codemagic Sample build task complete

Sylph Introduction

Sylph is a wrapper around AWS Device farm that helps run integration tests for Flutter projects. It orchestrates device farm instances (both Android & iOS) that will run the Flutter code we push to it.

flutter_driver dependency

The flutter_driver package is going to be required in order to test our project on emulated hardware automatically. To install this dependency to the project add the following to the dev_dependencies section of the pubspec.yaml file

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  test: ^1.5.2

test_driver setup

There also has to be test_driver configurations added to the repository that define what interactions should take place on the emulator. To do this create a new folder and two sub files

test_driver
    - main_test.dart
    - main.dart

Add the following to the files:

main_test.dart

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('end-to-end test', () {
    FlutterDriver driver;

    setUpAll(() async {
      // Connect to a running Flutter application instance.
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) await driver.close();
    });

    test('tap on the floating action button; verify counter', () async {
      // Finds the floating action button (fab) to tap on
      SerializableFinder fab = find.byTooltip('Increment');

      // Wait for the floating action button to appear
      await driver.waitFor(fab);

      // Tap on the fab
      await driver.tap(fab);

      // Wait for text to change to the desired value
      await driver.waitFor(find.text('1'));
    });
  });
}

main.dart

import 'package:device_farm_demo/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_driver/driver_extension.dart';

void main() {
  // Enable integration testing with the Flutter Driver extension.
  // See https://flutter.io/testing/ for more info.
  enableFlutterDriverExtension();
  runApp(MyApp());
}

Sylph Example Configuration

There’s a very good example in the Sylph repository that we’ll be tweaking for this demo. Create a new file in the root of the device_farm_demo project called sylph.yaml. Add the following contents to the file

# sylph config
tmp_dir: /tmp/sylph
artifacts_dir: /tmp/sylph_artifacts
# local timeout per device farm run
sylph_timeout: 720 # seconds approx
# run on ios and android pools concurrently (for faster results)
concurrent_runs: true

# device farm config
project_name: App Integration Tests
default_job_timeout: 10 # minutes, set at project creation

device_pools:

  - pool_name: android pool 1
    pool_type: android
    devices:
      - name: Google Pixel 2
        model: Google Pixel 2
        os: 8.0.0

  - pool_name: ios pool 1
    pool_type: ios
    devices:
      - name: Apple iPhone X
        model: A1865
        os: 11.4

test_suites:

  - test_suite: example tests 1
    main: test_driver/main.dart
    tests:
      - test_driver/main_test.dart
    pool_names:
      - android pool 1
      - ios pool 1
    job_timeout: 15 # minutes, set per job, over-rides default job timeout above

The contents of this file define the two device pools, one for iOS and the other for Android. Then it defines a test suite where it attaches the device pools to tests that should be run on the devices.

NOTE: If you don’t have a valid Apple Developer subscription you’ll have to remove the iOS device pool. You can do so by commenting them out.

device_pools:

  # - pool_name: ios pool 1
  #   pool_type: ios
  #   devices:
  #     - name: Apple iPhone X
  #       model: A1865
  #       os: 11.4

...

pool_names:
  - android pool 1
  # - ios pool 1

With all the required code now in the project, you can push the changed by running the following:

git add .
git commit -m "added sylph and test_driver"
git push

AWS Device Farm

AWS Device Farm is a service where real devices are exposed for you to run integration tests in life like scenarios. The service itself is only available in Oregon region (us-west-2).

AWS Authentication

To authenticate with AWS Device Farm we’ll have to create a new user with the correct permission scope. This is important especially since we are going to be putting these credentials in Codemagic and don’t want to over privilege them.

I’ve created a nice CloudFormation template that can be used to create a User, Group and Policy with the appropriate permission set attached. More information about the scope can be found on the offical documentation page.

Create a folder called aws and add a file to it called device_farm_user.yaml with the following contents

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  Password:
    NoEcho: 'true'
    Type: String
    Description: New account password
    MinLength: '1'
    MaxLength: '41'
    ConstraintDescription: the password must be between 1 and 41 characters

Resources:
  DeviceFarmUser:
    Type: AWS::IAM::User
    Properties:
      LoginProfile:
        Password: !Ref 'Password'
  DeviceFarmAdminGroup:
    Type: AWS::IAM::Group
  Admins:
    Type: AWS::IAM::UserToGroupAddition
    Properties:
      GroupName: !Ref 'DeviceFarmAdminGroup'
      Users: [!Ref 'DeviceFarmUser']
  DeviceFarmAccessPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: DeviceFarmList
      PolicyDocument:
        Statement:
        - Effect: Allow
          Action: ['devicefarm:*']
          Resource: '*'
      Groups: [!Ref 'DeviceFarmAdminGroup']
  DeviceFarmKeys:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName: !Ref 'DeviceFarmUser'

Outputs:
  AccessKey:
    Value: !Ref 'DeviceFarmKeys'
    Description: AWSAccessKeyId of new user
  SecretKey:
    Value: !GetAtt [DeviceFarmKeys, SecretAccessKey]
    Description: AWSSecretAccessKey of new user

Deploy the template by running the following command:

aws cloudformation create-stack \
  --stack-name iam-devicefarm-user \
  --template-body file://aws/device_farm_user.yaml \
  --parameters ParameterKey=Password,ParameterValue=$(openssl rand -base64 30) \
  --capabilities CAPABILITY_IAM

Retrieve the generated AWS Credentials by running the following two commands for each part of the set. Keep these values private.

# Get AWS Access Key ID
aws cloudformation describe-stacks --stack-name iam-devicefarm-user \
  --query 'Stacks[0].Outputs[?OutputKey==`AccessKey`].OutputValue' \
  --output text

# Get AWS Secret Access Key
aws cloudformation describe-stacks --stack-name iam-devicefarm-user \
  --query 'Stacks[0].Outputs[?OutputKey==`SecretKey`].OutputValue' \
  --output text

Codemagic Device Farm Setup

Finally we are able to setup Codemagic to use the credentials we just created to launch AWS Device Farm instances. Head back to Codemagic and go to the Settings page of the device_farm_demo project.

Expand the test step and select AWS Device Farm as the location in which to run the tests. Add the two secrets from the previous step to the boxes below that

AWS Device Farm credentials in Codemagic
AWS Device Farm credentials in Codemagic

iOS Build Environment [Optional]

For iOS builds you will also need to specify your Developer Portal Team ID. This can be found in the developer.apple.com/account/#/membership console.

Once you have retrieved your Developer Portal Team ID, create an Environment variable under the Codemagic settings for the device_farm_demo project:

  • TEAM_ID: ABCDEFGHIJ
Codemagic iOS Environment variable
Codemagic iOS Environment variable

Codemagic Run Build

Upon running a new build after setting up AWS Device Farm, Codemagic will use Sylph in order to create new devices on AWS to test against.

You can follow the progress either out of the Codemagic UI, or if you’re interested in what’s happening under the hood; navigate to the Device Farm console.

AWS Device Farm build overview
AWS Device Farm build overview

You should see the device pool we defined in the sylph.yaml file from earlier. You are also able to dig down into the test suite job Codemagic created for us as well.

AWS Device Farm test running status
AWS Device Farm test running status

Once the build completes you can view the status easily out of the Codemagic, or alternatively you can view the status in AWS Device Farm also.

Cleanup AWS

If you no longer want the user account / policy that we created earlier on in this post, you can:

  • Delete the CloudFormation stack from the CloudFormation console directly
  • Run the following command

    aws cloudformation delete-stack
    –stack-name iam-devicefarm-user


Check out more getting started guides and Flutter course by Nathan Glover in DevOpStar website.

Codemagic CI for Flutter