Testen speelt een cruciale rol tijdens de ontwikkeling van een app en kan, mits goed uitgevoerd, enorm veel tijd besparen. Het handmatig herhalen van tests is duur en tijdrovend; daarom zijn er testautomatisering frameworks in het leven geroepen. Appium is een van de populairste frameworks voor testautomatisering in de mobiele wereld. Het is een opensourceframework voor native, hybride en mobiele web-apps en is zeer goed gedocumenteerd. Appium is een krachtig framework waarmee je tests onafhankelijk van je app kunt schrijven. Tests kunnen worden geschreven in Java, Objective-C, JavaScript (Node), PHP, Python, Ruby, C#, Clojure of Perl met behulp van de Selenium WebDriver API en taalspecifieke client-libraries.
Hoe gebruik je Appium?
Appium wordt geleverd met drie tools:
Appium server
De Appium server is een HTTP REST-API die verantwoordelijk is voor het detecteren van en communiceren met testapparaten. Appium ondersteunt zowel emulators als fysieke apparaten. Tegenwoordig is Appium geïntegreerd in GitLab CI, AWS, Bitrise en andere cloudapplicaties. Indien nodig kun je de API ook gebruiken om volledig je eigen integratie te schrijven.
Appium desktop
Desktop is een app voor Mac, Windows en Linux die de kracht van de Appium automatiseringsserver verpakt in een mooie en flexibele UI.
Appium Inspector
Appium inspector is een tool waarmee je elementen van een mobiele app kunt inspecteren. Bij het schrijven van mobiele tests moet je UI-elementen identificeren aan de hand van bepaalde ID's. Met Appium inspector kun je die ID's vinden zonder dat je de broncode nodig hebt.
Zodra de Appium server draait, kun je communiceren met de API via een van de eerder genoemde programmeertalen of via Appium inspector.
Appium Desired Capabilities: Wat zijn dat?
Voordat je begint met testen, moet je begrijpen wat de Desired Capabilities van Appium zijn. Dit is in wezen een JSON-document dat door de Appium-client naar de server wordt gestuurd zodra er een nieuwe automatiseringssessie wordt aangevraagd. Dit document bevat cruciale gegevens over hoe je sessie moet verlopen. Denk hierbij aan het apparaat dat je wilt gebruiken, welke app er gestart moet worden, of de app vooraf verwijderd moet worden en welke browser je wilt testen. Appium ondersteunt vrijwel alle emulators, fysieke apparaten en browsers.
Configureren voor Android en iOS
Voor een Android-emulator (zoals een gesimuleerde Samsung Galaxy A40) specificeer je de platformgegevens, de apparaatnaam en de UDID. Daarnaast vereist Android een pad naar je APK-bestand via de eigenschap "app", óf twee specifieke eigenschappen die het app-pakket en de opstartactiviteit van de app definiëren (zoals gedefinieerd in je AndroidManifest.xml).
Bij iOS moeten fysieke apparaten eerst worden geprovisioned. In de JSON-configuratie gebruik je hier specifieke eigenschappen voor, zoals xcodeOrgId en xcodeSigningId. In plaats van de bundleId kun je hier ook de eigenschap "app" gebruiken om rechtstreeks naar een IPA-bestand te verwijzen. Een typische configuratie ziet er als volgt uit:
JSON
{
"appium:platformName": "Android",
"appium:platformVersion": "11",
"appium:deviceName": "Samsung Galaxy A40",
"appium:udid": "******",
"appium:appPackage": "com.coffeeit.appium_example",
"appium:appActivity": "com.coffeeit.appium_example.MainActivity",
"appium:noReset": "false",
"appium:fullReset": "false"
}
Wat doen "noReset" and "fullReset"?
Beginnende Appium-gebruikers raken vaak in de war door de keys "noReset" en "fullReset". Deze eigenschappen instrueren Appium om de app-gegevens te wissen of de app volledig te verwijderen voordat een test start. Dit is bijvoorbeeld ontzettend handig wanneer je de onboarding-flow van een app herhaaldelijk vanaf een schone lei wilt testen.
- Als
noResetoptruestaat, worden alle app-gegevens gewist. - Als
fullResetoptruestaat, wordt de app verwijderd en opnieuw geïnstalleerd (dit is alleen mogelijk als er een pad naar een APK/IPA is opgegeven).
Persoonlijk wis ik altijd de app-gegevens, omdat je niet blind kunt vertrouwen op de status van een app. Het wissen van de app-gegevens geeft een test een hogere slaagkans.
Accessibility ID's
Bij het schrijven van tests moeten we communiceren met UI-elementen. Er zijn verschillende manieren om dit te doen. UI-elementen zijn herkenbaar aan:
- ID
- Accessibility ID
- XPath: Het geneste pad van een element
- Class name: De klassenaam van het element
- Tag name
- En diverse andere Android/iOS-gerelateerde methoden
De beste manier is om accessibility ID's te gebruiken. Simpelweg omdat accessibility ID's geen codewijzigingen vereisen en gesynchroniseerd kunnen worden tussen meerdere apps. Dus als je een Android- en een iOS-app hebt, kun je (afhankelijk van hoeveel ze van elkaar verschillen) dezelfde test voor beide apps gebruiken. Dit betekent dat je geen aparte test hoeft te maken voor Android en een andere voor iOS. Bovendien kun je if-statements in je code gebruiken om UI-bewerkingen uit te voeren die alleen voor iOS of alleen voor Android gelden.
Tests schrijven met Kotlin
Nu we begrijpen wat de Desired Capabilities van Appium zijn en een basisbegrip hebben van hoe we UI-elementen kunnen identificeren, kunnen we beginnen met het schrijven van tests. In deze blogpost gebruik ik Kotlin.
Stap 1: Afhankelijkheden (Dependencies) importeren
Eerst moeten we een aantal vereiste afhankelijkheden in ons project importeren:
Kotlin
dependencies {
implementation("io.appium:java-client:8.0.0")
implementation("org.seleniumhq.selenium:selenium-java:4.1.4")
implementation("org.testng:testng:7.5")
testImplementation("org.testng:testng:7.5")
}
Stap 2: De testklasse aanmaken
Voor dit voorbeeld test ik een eenvoudige taken-app die ik heb gemaakt. Maak een nieuwe klasse aan in src/test/kotlin genaamd TodoTasksTest die er als volgt uitziet:
Kotlin
import io.appium.java_client.AppiumBy
import io.appium.java_client.android.AndroidDriver
import org.openqa.selenium.WebDriver
import org.openqa.selenium.remote.DesiredCapabilities
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait
import org.testng.annotations.AfterTest
import org.testng.annotations.BeforeTest
import org.testng.annotations.Test
import java.net.URL
import java.time.Duration
class TodoTasksTest {
private lateinit var driver: WebDriver
private lateinit var waiter: WebDriverWait
@BeforeTest
fun setup() {
// Setup the desired capabilities
val capabilities = DesiredCapabilities()
capabilities.setCapability("platformName", "Android")
capabilities.setCapability("deviceName", "Samsung Galaxy A40")
capabilities.setCapability("platformVersion", "11")
capabilities.setCapability("udid", "******")
capabilities.setCapability("appPackage", "com.coffeeit.appium_example")
capabilities.setCapability("appActivity", "com.coffeeit.appium_example.MainActivity")
capabilities.setCapability("noReset", "false")
capabilities.setCapability("fullReset", "false")
driver = AndroidDriver(URL("http://127.0.0.1:4723/wd/hub"), capabilities)
waiter = WebDriverWait(driver, Duration.ofSeconds(10L))
}
@Test
fun testAddTask() {
// wait for the element to be visible and click on it
waiter.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("AddTask"))).click()
// if accessibility ids are not defined, we can identify elements in different ways
val elements = driver.findElements(AppiumBy.className("android.widget.EditText"))
elements[0].sendKeys("Some title")
elements[1].sendKeys("Some description")
// wait for the element to be visible and click on it
waiter.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("SaveTask"))).click()
}
@Test
fun testAddTaskBackButton() {
// wait for the element to be visible and click on it
waiter.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("AddTask"))).click()
// wait for the element to be visible and click on it
waiter.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("Back"))).click()
}
@AfterTest
fun teardown() {
driver.quit()
}
}
In het bovenstaande voorbeeld beginnen we met het instellen van onze capabilities. Daarna zijn er twee tests gedefinieerd: de ene voegt een nieuwe taak toe met een titel en beschrijving, terwijl de andere op de terugknop drukt wanneer er wordt geprobeerd een taak toe te voegen. In de laatste functie beëindigen we de automatiseringssessie.
De langste stap hier is het instellen van de capabilities en het voorbereiden van het testapparaat. De tests zelf duren slechts een paar seconden.De volgende keer dat we iets willen testen, starten we simpelweg de Appium server, sluiten we ons apparaat aan en voeren we onze tests uit. Maar wat als je geen developer bent? Kun je Appium dan nog steeds gebruiken om tests te schrijven?
Introductie tot TestProject
Appium is een gratis, opensourceframework dat ontworpen is om testen zo veel mogelijk te automatiseren, en dat doet het uitstekend. Je moet echter nog steeds een developer zijn om de tests te kunnen schrijven.
Gelukkig bieden commerciële testautomatiseringsframeworks online omgevingen waarin je apps kunt testen zonder code te schrijven. Je kunt testdata klaarzetten, tests opnemen en ze integreren in cloudoplossingen zoals GitLab CI of AWS.
In 2015 begon een opensource-initiatief om deze premium functies gratis voor de wereld beschikbaar te maken. In april 2018 werd de eerste versie van deze online testomgeving uitgebracht onder de naam TestProject.TestProject is een gratis, end-to-end testautomatiseringsplatform voor web-, mobiele en API-tests dat slim gebruikmaakt van Appium. Het is een zeer krachtig platform dat volledig gratis kan worden gebruikt.
Belangrijke kernoverwegingen
Hoewel TestProject beweert opensource te zijn, zijn in werkelijkheid alleen hun SDK en plug-ins dat. De online omgeving zelf is closed-source. Dit betekent dat je geen lokale instantie van het online dashboard kunt draaien en dat een actieve internetverbinding verplicht is om het te gebruiken.
Hoe gebruik je TestProject?
Het gebruik van TestProject is heel eenvoudig:
- Registreer een account en log in.
- Volg de installatiewizard en bevestig je e-mailadres.
- Stel de TestProject Agent in. Deze agent draait een Appium server op de achtergrond en communiceert rechtstreeks met je testapparaten en emulators.
- Eenmaal geconfigureerd, kom je in de testomgeving terecht waar automatisch een standaard testproject wordt aangemaakt om je op weg te helpen.
Navigeren op het TestProject-dashboard
Linksboven op het dashboard zie je je huidige project en kun je naar andere projecten schakelen. De tabbladen eronder beheren je projectspecifieke gegevens:
Tests & Jobs
TestProject verdeelt het werk in tests en jobs. Tests volgen simpelweg een specifieke stroom van stappen, terwijl jobs bestaan uit een of meerdere tests die aan een specifiek apparaat zijn gekoppeld. Jobs kunnen handmatig worden geactiveerd of worden ingepland.
Elements
In het tabblad Elements kun je UI-elementen vooraf definiëren. Dit maakt het schrijven van meerdere tests een stuk eenvoudiger, omdat je er later simpelweg naar kunt verwijzen zonder opnieuw code te hoeven schrijven.
Applications & Data Sources
- Applications: Definieer de specifieke apps die je wilt testen.
- Data Sources: Sla externe bestanden op (zoals een CSV-bestand) om de weg vrij te maken voor Data-Driven Tests en Jobs.
Parameters & Devices
- Parameters: Definieer key/value-parameters op projectniveau (zoals inloggegevens) om ze in verschillende tests te hergebruiken.
- Devices: Geef autorisatie aan je testhardware. Wanneer de agent een nieuw apparaat herkent, verschijnt er een pop-up voor autorisatie.
Geavanceerde test- & stapconfiguratie
Wanneer je een test opent, wordt alle gerelateerde informatie weergegeven. Je kunt stappen native opnemen (hiervoor moet een lokale TestProject agent actief zijn) of handmatig stappen aanmaken.
Globale versus stap-specifieke instellingen
Het tandwielpictogram linksboven opent de globale testinstellingen. Hier kun je het volgende configureren:
- Hoe snel de test moet worden uitgevoerd.
- Wat er gebeurt als een stap mislukt.
- Of er bij elke stap screenshots moeten worden gemaakt.
Hoewel deze regels globaal gelden, kun je ze binnen TestProject ook op stapniveau overschrijven door de geavanceerde opties van een specifieke individuele stap uit te vouwen.
Binnen het tabblad Steps
Elke stap in een test heeft een specifieke actie die moet worden uitgevoerd. TestProject biedt een breed scala aan ingebouwde opties en ondersteunt add-ons.Add-ons zijn ontzettend handig wanneer je moet communiceren met native UI-elementen buiten je app, en ze kunnen door iedereen in de community worden gemaakt en gedeeld.
Platformrapporten en integraties
Prachtige rapportages
TestProject kan automatisch visueel aantrekkelijke, gedetailleerde testrapporten genereren. Deze lichten de testuitvoering van begin tot eind toe, waarbij de exact gebruikte testdata en uitgevoerde acties worden benadrukt.Samenvattingen en volledige rapporten kunnen eenvoudig worden gedeeld met stakeholders. Als je test is geconfigureerd om bij elke stap screenshots te maken, worden die afbeeldingen rechtstreeks in het volledige rapport gekoppeld.
Integraties van derden
TestProject integreert native met platforms zoals Sauce Labs en BrowserStack, en ondersteunt aangepaste webhooks voor succes- of foutmeldingen.Hoewel TestProject-agents binnen Docker-containers kunnen draaien, moet je er rekening mee houden dat op Docker gebaseerde agents momenteel geen lokale hardware-apparaatherkenning ondersteunen.
API & GitLab CI-pipelines
TestProject beschikt over een omvangrijke API waarmee je eenvoudig tests en jobs op afstand kunt triggeren. Je kunt bijvoorbeeld je eigen aangepaste integratiepipeline schrijven in GitLab CI voor het testen van Android. Deze reeks voert een serie jobs uit en sluit de pipeline pas af wanneer alle taken succesvol zijn afgerond:
YAML
# Minimal OS image with Java and Android 31 build tools pre-installed
image: mreichelt/android:31
variables:
# Colors for terminal output
Color_Off: '\e[0m'
Red: '\e[0;31m'
Green: '\e[0;32m'
BRed: '\e[1;31m'
BGreen: '\e[1;32m'
BBlue: '\e[1;34m'
# TestProject variables
TP_API_URL: "https://api.testproject.io/v2"
TEST_RETRIES: 2
TP_API_KEY: "SOME_API_KEY"
TP_PROJECT_ID: "some_project_id"
TP_APP_ID: "some_app_id"
TP_APK_FILE_NAME: "app-debug.apk"
TP_JOBS: "job_id_1 job_id_2"
before_script:
- apt-get --quiet update --yes
- apt-get --quiet install jq --yes
stages:
- build
- upload
- test
# Build project
assembleDebug:
interruptible: true
stage: build
script:
- ./gradlew -Dorg.gradle.jvmargs=-Xmx2048m assembleDebug
artifacts:
untracked: true
paths:
- app/build/outputs/
# Upload the APK to TestProject
uploadAPK:
interruptible: true
stage: upload
script:
# Get AWS url from TestProject to upload to
- 'UPLOAD_DATA=$(curl -s -S -X GET "${TP_API_URL}/projects/${TP_PROJECT_ID}/applications/${TP_APP_ID}/file/upload-link" -H "accept: application/json" -H "Authorization: ${TP_API_KEY}")'
- echo $UPLOAD_DATA
# Parse the upload url
- UPLOAD_URL=$(echo $UPLOAD_DATA | jq -r ".url")
- UPLOAD_METHOD=$(echo $UPLOAD_DATA | jq -r ".method.Method")
- echo $UPLOAD_URL
- echo $UPLOAD_METHOD
# Upload the APK to the upload url
- curl "${UPLOAD_URL}" --upload-file app/build/outputs/apk/debug/app-debug.apk
# Publish the APK to the TestProject app
- 'UPLOAD_CONFIRM_DATA=$(curl -X POST "${TP_API_URL}/projects/${TP_PROJECT_ID}/applications/${TP_APP_ID}/file" -H "accept: application/json" -H "Authorization: ${TP_API_KEY}" -H "Content-Type: application/json" -d "{ \"fileName\": \"${TP_APK_FILE_NAME}\" }")'
.avif)



