Een Flutter App - Deel 3 - CI Nog Geen CD

Het opzetten van Continuous Integration pipelines met Github Actions

Flutter
Android
DevOps
CI
Github Actions
Author

Mees Molenaar

Published

September 27, 2022

Intro

Hallootjes,

welkom bij de volgende stap in het proces om een Flutter app te maken. In de vorige post heb ik de app-structuur bepaald, maar voordat wij daaraan beginnen gaan we eerst een Continuous Integration (CI) pipeline opzetten. Deze pipeline zorgt ervoor dat wanneer er nieuwe code wordt gepushed er automatisch wordt gecontroleerd of de bestaande code en app nog naar behoren werken. Dit doe je door tests te schrijven die vervolgens worden uitgevoerd door je pipeline.

PS: Hier is de Github repository

In Flutter heb je drie soorten tests: integration, widget en unit tests. Laten we eerst kijken naar de integration tests. Dit zijn tests om de gehele app te testen. Deze test repliceert het gedrag van een gebruiker en is wat mij betreft daarom ook de belangrijkste soort test. Deze tests moeten altijd werken om te garanderen dat je app hetzelfde functioneert. Ten tweede, widget test: dit zijn tests (zoals de naam al doet vermoeden) om widgets, onderdelen van de User Interface (UI) te testen. En als laatste unit tests. Deze tests zijn handig om de werkzaamheid van losse functies of classes te testen. En deze tests zouden zo simpel moeten zijn dat ze zelfs op de pc van je oma kunnen draaien (ben de bron van deze quote kwijt). Maar waarom testen wij eigenlijk?

Er zijn een legio voordelen aan het automatisch testen van code, zoals het besparen van kosten (omdat je (minder) manueel hoeft te testen), sneller developen en deployen en een hogere code qualiteit. Lees voor nog meer voordelen en adviezen over automatisch testen eens The DevOps Handbook. Maar de belangrijkste van alles is het korter maken van de feedback loop. Als developer is er namelijk niets frustrerender dan dat je code hebt gemaakt waarna je maanden later hoort dat er iets niet werkte. Het zou veel beter zijn om direct te weten dat er iets niet meer werkt zodat je na jou aanpassing/toevoeging het probleem direct kan oplossen en daar dan ook van te leren. In tegenstelling tot lang wachten want dan weet je niet eens meer waarom je dat stukje code hebt geschreven laat staan hoe je het kunt oplossen.

Github Actions

Voor deze CI pipelines ga ik Github Actions gebruiken. Het voordeel hiervan is dat de code, het project board en de pipelines allemaal in één omgeving staan (namelijk Github). Daarnaast zijn Github Actions voor Publieke repositories gratis (voor Private repositories heb je een aantal gratis minuten per maand en daarna betaal je voor de minuten die je extra gebruikt)! Github Actions heeft zogeheten Runners (een virtuele machine die je pipeline uitvoert) met verschillende Operation Systems (OS). Omdat je geen Android of IPhone emulator op een Linux machine kunt draaien, gebruiken we daarvoor macOS runners (let op! in private repos kunnen de kosten van deze runners snel oplopen! Ze zijn namelijk 10x zo duur). Deze emulators zijn nodig om de integratie tests uit te voeren. Maar laten we eerst de eenvoudige tests bespreken: de unit en widget tests.

Unit en Widget Tests

Voor de Unit en Widget tests heb je weinig nodig. Eigenlijk alleen een computer met Flutter geïnstalleerd en dan kan je gemakkelijk de volgende command uitvoeren:

flutter test

Deze stappen uit voeren met een Github Action zijn ook gemakkelijk! Als eerste moet je Flutter installeren. Op de Github Marketplace heeft een gebruiker een Action gedeeld waarmee je de gewenste Flutter versie kan installeren. Vervolgens installeer je de packages en dan test je (voorbeeld hieronder). Deze YAML bestander sla je op onder de folders .github/workflows en deze workflow zal af gaan iedere keer dat er code naar main wordt gepushed.

name: Flutter Unit And Widget Tests

# Perform action when code is pushed to the main branch
on:
  push:
    branches: [main]

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      # First checkout the new code
      - name: Checkout the code
        uses: actions/checkout@v3

      # Use a handy action from the Github marketplace to install flutter
      - name: Install and set Flutter version
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.3.10"
          channel: "stable"

      - name: Show Flutter version
        run: flutter --version

      - name: Get Flutter packages
        run: flutter pub get

      - name: Analyze the code
        run: flutter analyze

      - name: Run unit tests with coverage
        run: flutter test --coverage

Voorbeeld is gebaseerd op: Run Flutter tests using GitHub Actions and Codecov

Web Integration Tests

De volgende makkelijk op te zetten tests zijn web integratie testen. Deze draaien namelijk met een Chrome Driver die je ook op Linux machines kunt installeren. Hieronder is een voorbeeld voor een Github Workflow dat web integration tests uitvoert. Maar voordat je deze kunt uitvoeren, heb je een klein Dart bestandje nodig die de integrationDriver start (zie hieronder) (klik hier voor extra informatie). Vervolgens kan je met de command:

flutter drive

de web integration test starten. Het nadeel is wel dat je het bestand dat je wilt testen moet specificeren. Dat resulteert in één groot integration test bestand. Zelf vind ik het fijner om tests te verdelen in verschillende bestanden, maar daar is helaas voor deze web integration tests nog geen oplossing voor.

name: Flutter Web Integration Tests

# Perform action when code is pushed to the main branch
on:
  push:
    branches: [main]

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      # First checkout the new code
      - name: Checkout the code
        uses: actions/checkout@v3

      # Use a handy action from the Github marketplace to install flutter
      - name: Install and set Flutter version
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.3.10"
          channel: "stable"

      - name: Show Flutter version
        run: flutter --version

      - name: Get Flutter packages
        run: flutter pub get

      - name: Start Chromedriver
        run: chromedriver --port=4444 &

      - name: Run Web integration test
        run: flutter drive --driver=test_driver/integration_test.dart --target=integration_test/integration_test.dart -d web-server

Voorbeeld is gebaseerd op: Run Flutter Integration Tests in GitHub Actions

IPhone Integration Tests

Nu zijn we aangekomen bij de complexere integration tests (en ook bij de duurdere wanneer je een Private repo hebt). Deze tests worden namelijk uitgevoerd op een macOS runner. Ook zit er een klein stukje code in de Github Action dat ervoor zorgt dat je de juiste UDID krijgt van de IPhone Emulator. Met deze UDID kan je dan succesvol de integration testen uitvoeren (zie voorbeeld Action hieronder). In het geval van deze integration tests kun je de folder waar de tests zich bevinden aangeven i.p.v. een bestand zoals met de web integration tests. Hierdoor kan je de tests wel verdelen in verschillende bestanden.

name: flutter iphone integration test

# Perform action when code is pushed to the main branch
on:
  push:
    branches: [main]

jobs:
  iphone_integration_test:
    # NOTE: Running on macOS
    runs-on: macOS-latest

    # The device name is saved in an environment variable
    # we use this environment variable to search for the device UDID
    env:
      device: 'iPhone 13 Simulator \(16.0\)'

    steps:
      # We list the simulators for debugging purposes
      # when the device in the environment variable is not in this list
      # we have to change it to one that is in the list
      - name: List all simulators
        run: xcrun xctrace list devices

      # First checkout the new code
      - name: Checkout the code
        uses: actions/checkout@v3

      # Use a handy action from the Github marketplace to install flutter
      - name: Install and set Flutter version
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.3.10"
          channel: "stable"

      - name: Show Flutter version
        run: flutter --version

      - name: Get Flutter packages
        run: flutter pub get

      # We first get the UDID of the device with an awk command
      # Then we boot that device and use that device for the integration test with the -d flag
      - name: Get UDID device, Start Simulator and Connect flutter
        run: |
          UDID=$(xcrun xctrace list devices | awk -F " " '/${{ env.device }}/ && length($5) > 1 {print $5}' | tr -d '()')
          echo $UDID
          xcrun simctl boot "${UDID:?No Simulator with this name found}"
          flutter test integration_test -d $UDID

Voorbeeld is gebaseerd op: Run Flutter Driver tests on GitHub Actions

Android Integration Tests

Ook de Android integration tests kan je enkel draaien op een macOS runner. Omdat Android Gradle gebruikt om je app te builden (zo heet dat) heb je Java versie 11 of hoger nodig. Wanneer je die actie hebt uitgevoerd installeer je wederom Flutter. Als laatste gebruiken we een actie van ReactiveCircus om een Android Emulator te starten. Aan deze actie moeten de minimum api-leven en het uit te voeren script worden mee gegeven.In dit geval is het script een losse command.

name: flutter android integration test

# Perform action when code is pushed to the main branch
on:
  push:
    branches: [main]

jobs:
  android_integration_test:
    runs-on: macOS-latest

    steps:
      # First checkout the new code
      - name: Checkout the code
        uses: actions/checkout@v3

      # We need atleast Java 11 or Higher to build your app with this version of Gradle
      # So we use this Marketplace Action to install it
      - name: Setup Java JDK
        uses: actions/setup-java@v3.5.0
        with:
          distribution: "zulu"
          java-version: "11.x"

      # Use a handy action from the Github marketplace to install flutter
      - name: Install and set Flutter version
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.3.10"
          channel: "stable"

      - name: Show Flutter version
        run: flutter --version

      - name: Get Flutter packages
        run: flutter pub get

      # Use a Github Action from the Marketplace to start an Android Emulator with api-level 23
      - name: Start Emulator And Start Tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 23
          script: flutter test integration_test

Nu de verschillende pipelines zijn opgezet push ik dit naar Github en zullen deze de tests draaien wanneer ik nieuwe code naar Github push. Daarvoor zijn natuurlijk nog wel de afzonderlijke unit, widget en integratie tests nodig! Dus deze zullen snel volgen.

Dit was het voor nu, tot de volgende keer en geniet van de herfst :).

Mees