GitHub Actions provides the infrastructure for running mobile tests on every PR and on a nightly schedule. This lesson covers a working workflow for Android emulator tests, optional BrowserStack integration, and Allure report publishing.
Workflow for Android emulator tests
name: Mobile Smoke Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
android-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
- name: Install Python dependencies
run: pip install -r requirements.txt
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Appium
run: |
npm install -g appium
appium driver install uiautomator2
- name: Run tests with Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
target: google_apis
arch: x86_64
profile: pixel_6
disable-animations: true
script: |
appium --base-path / --log appium.log &
sleep 5
pytest -m smoke --alluredir=allure-results
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: allure-results
path: allure-results
- name: Upload Appium log on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: appium-log
path: appium.logProgrammatic Appium vs background process
Background process (shown above):
appium --base-path / --log appium.log &
sleep 5The sleep 5 is a blunt wait for server startup. Replace with a health check loop:
appium --base-path / --log appium.log &
timeout 30 bash -c 'until curl -sf http://127.0.0.1:4723/status; do sleep 1; done'Programmatic (preferred for clean teardown):
Use subprocess in a session-scoped fixture:
@pytest.fixture(scope="session", autouse=True)
def appium_server():
import subprocess
import time
proc = subprocess.Popen(["appium", "--base-path", "/"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(4)
yield
proc.terminate()Nightly regression on BrowserStack
name: Nightly Regression
on:
schedule:
- cron: '0 2 * * 1-5' # 2am UTC Mon-Fri
jobs:
regression-browserstack:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- name: Upload app to BrowserStack
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
run: |
APP_URL=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \
-X POST https://api-cloud.browserstack.com/app-automate/upload \
-F "file=@apps/app-debug.apk" | python3 -c "import sys,json; print(json.load(sys.stdin)['app_url'])")
echo "BROWSERSTACK_APP_URL=$APP_URL" >> $GITHUB_ENV
- name: Run regression suite
env:
BROWSERSTACK: "true"
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
BUILD_NUMBER: ${{ github.run_number }}
run: pytest -m regression -n 2 --alluredir=allure-results
- uses: actions/upload-artifact@v4
if: always()
with:
name: allure-results
path: allure-resultsSecrets configuration
Store credentials in GitHub repository settings under Settings > Secrets and variables > Actions:
BROWSERSTACK_USERNAMEBROWSERSTACK_ACCESS_KEYSLACK_WEBHOOK_URL(optional)
Reference them in workflows as ${{ secrets.SECRET_NAME }}.
Publishing Allure to GitHub Pages
- name: Generate Allure Report
if: always()
run: allure generate allure-results -o allure-report --clean
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main' && always()
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: allure-reportThe report is available at https://<org>.github.io/<repo>/ after each main branch run.
Disabling animations in CI
Animations cause flake by adding non-deterministic delays to transitions:
# conftest.py
import os
@pytest.fixture(scope="session", autouse=True)
def disable_animations(driver):
"""Disable Android animations in CI."""
if os.getenv("CI"):
for scale in ["window_animation_scale", "transition_animation_scale",
"animator_duration_scale"]:
driver.execute_script("mobile: shell", {
"command": f"settings put global {scale} 0"
})
yield
# Restore animations after the session
if os.getenv("CI"):
for scale in ["window_animation_scale", "transition_animation_scale",
"animator_duration_scale"]:
driver.execute_script("mobile: shell", {
"command": f"settings put global {scale} 1"
})The reactivecircus/android-emulator-runner action also supports disable-animations: true which achieves the same effect at the runner level.
Slack notification on failure
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": ":x: Mobile tests failed on ${{ github.ref_name }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Mobile Tests Failed*\nBranch: `${{ github.ref_name }}`\n<${{ github.run_url }}|View run>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}