Skip to content

Commit f3b3205

Browse files
initial commit
0 parents  commit f3b3205

13 files changed

Lines changed: 574 additions & 0 deletions

.github/workflows/test-python.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Validate RobotFramework App Example
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths-ignore:
8+
- '**.md'
9+
pull_request:
10+
paths-ignore:
11+
- '**.md'
12+
jobs:
13+
test-robotframework:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.12'
20+
cache: 'pip'
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
- run: |
26+
PYTHONPATH=$PYTHONPATH:. robot \
27+
--variable PLATFORM:Android \
28+
--variable VERSION:14.0 \
29+
--variable DEVICE:Pixel.* \
30+
--variable APP:https://testingbot.com/appium/sample.apk \
31+
test.robot
32+
env:
33+
TB_KEY: ${{ secrets.TB_KEY }}
34+
TB_SECRET: ${{ secrets.TB_SECRET }}
35+
- name: Upload Robot Framework reports
36+
if: failure()
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: robot-reports
40+
path: |
41+
log.html
42+
report.html
43+
output.xml

.gitignore

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pyenv
86+
# For a library or package, you might want to ignore these files since the code is
87+
# intended to run in multiple environments; otherwise, check them in:
88+
# .python-version
89+
90+
# pipenv
91+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94+
# install all needed dependencies.
95+
#Pipfile.lock
96+
97+
# poetry
98+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99+
# This is especially recommended for binary packages to ensure reproducibility, and is more
100+
# commonly ignored for libraries.
101+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102+
#poetry.lock
103+
104+
# pdm
105+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106+
#pdm.lock
107+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108+
# in version control.
109+
# https://pdm.fming.dev/#use-with-ide
110+
.pdm.toml
111+
112+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113+
__pypackages__/
114+
115+
# Celery stuff
116+
celerybeat-schedule
117+
celerybeat.pid
118+
119+
# SageMath parsed files
120+
*.sage.py
121+
122+
# Environments
123+
.env
124+
.venv
125+
env/
126+
venv/
127+
ENV/
128+
env.bak/
129+
venv.bak/
130+
131+
# Spyder project settings
132+
.spyderproject
133+
.spyproject
134+
135+
# Rope project settings
136+
.ropeproject
137+
138+
# mkdocs documentation
139+
/site
140+
141+
# mypy
142+
.mypy_cache/
143+
.dmypy.json
144+
dmypy.json
145+
146+
# Pyre type checker
147+
.pyre/
148+
149+
# pytype static type analyzer
150+
.pytype/
151+
152+
# Cython debug symbols
153+
cython_debug/
154+
155+
# PyCharm
156+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158+
# and can be added to the global gitignore or merged into this file. For a more nuclear
159+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160+
#.idea/

AppOptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from appium.options.android import UiAutomator2Options
2+
from appium.options.ios import XCUITestOptions
3+
4+
5+
def get_app_options(platform):
6+
if platform.lower() == "android":
7+
return UiAutomator2Options()
8+
elif platform.lower() == "ios":
9+
return XCUITestOptions()
10+
else:
11+
raise ValueError(f"Unsupported platform: {platform}")

CLAUDE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Purpose
6+
7+
Example project showing how to drive a remote Appium session on the TestingBot cloud device grid from Robot Framework to run native mobile app tests. It is intentionally small — one suite (`test.robot`) plus two Python helper libraries.
8+
9+
## Running tests
10+
11+
`PYTHONPATH` must include the repo root so Robot Framework can import the local Python libraries (`AppOptions.py`, `TestingBot.py`) as libraries named `AppOptions` / `TestingBot`.
12+
13+
```
14+
pip install -r requirements.txt
15+
export TB_KEY=<key>
16+
export TB_SECRET=<secret>
17+
PYTHONPATH=$PYTHONPATH:. robot \
18+
--variable PLATFORM:Android \
19+
--variable VERSION:14.0 \
20+
--variable DEVICE:Pixel.* \
21+
--variable APP:https://testingbot.com/appium/sample.apk \
22+
test.robot
23+
```
24+
25+
Run a single test case: add `--test "Simple App Test"` (or `-t`) to the `robot` invocation.
26+
27+
`TB_KEY` / `TB_SECRET` are read from `os.environ` inside `TestingBot.py`**do not** pass them via `--variable` or embed them in the remote URL, or they will leak into `log.html` / `output.xml`. The four `--variable` args (`PLATFORM`, `VERSION`, `DEVICE`, `APP`) are required — the suite references them with no defaults. CI (`.github/workflows/test-python.yml`) injects `TB_KEY`/`TB_SECRET` via the job's `env:` block from GitHub secrets.
28+
29+
## Architecture
30+
31+
The suite is three pieces that plug together via Robot Framework's `Library` imports:
32+
33+
- **`test.robot`** — declares `AppiumLibrary`, `AppOptions`, and `TestingBot` as libraries. `Test Setup` calls `Get App Options` to get an Appium platform-specific `*Options` object (e.g. `UiAutomator2Options` / `XCUITestOptions`), attaches W3C + Appium capabilities and a `tb:options` dict (TestingBot-specific metadata like build name) via `Call Method ... set_capability`, then hands the configured options off to `Open Testingbot App`. The suite never references credentials.
34+
35+
- **`AppOptions.py`** — factory returning `UiAutomator2Options` / `XCUITestOptions` based on the `PLATFORM` variable. Keeping this in Python (rather than Robot keywords) is deliberate: the test needs a real Appium options *object* to call `set_capability` on.
36+
37+
- **`TestingBot.py`** — owns all TestingBot/credential concerns. Reads `TB_KEY`/`TB_SECRET` from `os.environ`, builds a `selenium.webdriver.remote.client_config.ClientConfig` with basic auth, and creates the remote Appium driver via `appium.webdriver.Remote(...)`. It then registers the driver in the running `AppiumLibrary` instance's `_cache` — this both avoids URL-embedded credentials (which would be logged) and means subsequent AppiumLibrary keywords (`Wait Until Page Contains Element`, `Click Element`, …) just work. `Report Testingbot Status` then reaches into the running `AppiumLibrary` instance for the live `session_id` and PUTs pass/fail to `api.testingbot.com/v1/tests/<session_id>`. This only works while the app session is still open, which is why it runs *before* `Close Application` in the teardown.
38+
39+
Note: `AppiumLibrary`'s own `Open Application` keyword also creates the driver, but only accepts capability kwargs and would require embedding credentials in the remote URL. Registering our own driver in `appium._cache` is the escape hatch that keeps credentials out of the URL and the logs.
40+
41+
When changing capabilities or adding platform-specific flags, edit `test.robot` (capabilities) or `AppOptions.py` (options object construction) — don't try to do it purely in Robot syntax. If you need to change how the driver is created or how auth is handled, that all lives in `TestingBot.py:open_testingbot_app` — keep credentials out of `test.robot` and out of Robot variables.
42+
43+
## Ignorable artifacts
44+
45+
`log.html`, `report.html`, `output.xml`, `.pabotsuitenames`, and `pabot_results/` are generated by `robot`/`pabot` runs and should not be committed or treated as source.

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) TestingBot.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be
12+
included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[![Validate RobotFramework App Example](https://github.com/testingbot/robotframework-app-example/actions/workflows/test-python.yml/badge.svg)](https://github.com/testingbot/robotframework-app-example/actions/workflows/test-python.yml)
2+
3+
## TestingBot RobotFramework Appium Example
4+
5+
TestingBot provides an online grid of real devices and emulators/simulators to run automated native app tests on via Appium.
6+
This example demonstrates how to use RobotFramework together with [AppiumLibrary](https://docs.robotframework.org/docs/different_libraries/appium) to run a native mobile app test on a remote TestingBot device.
7+
8+
### Environment Setup
9+
10+
1. Setup
11+
* Clone the repo
12+
* Install the dependencies `pip install -r requirements.txt`
13+
14+
2. TestingBot Credentials
15+
16+
Retrieve your TestingBot Key and Secret from the [TestingBot Dashboard](https://testingbot.com/members/) and export them as environment variables:
17+
18+
```
19+
export TB_KEY=<your TestingBot Key>
20+
export TB_SECRET=<your TestingBot Secret>
21+
```
22+
23+
3. Upload your app
24+
25+
Upload your `.apk` or `.ipa` to TestingBot (or host it on a public URL). See [TestingBot App Automate](https://testingbot.com/support/app-automate/help).
26+
27+
4. Run test:
28+
29+
Android example:
30+
```
31+
PYTHONPATH=$PYTHONPATH:. robot \
32+
--variable PLATFORM:Android \
33+
--variable VERSION:14.0 \
34+
--variable DEVICE:Pixel.* \
35+
--variable APP:https://testingbot.com/appium/sample.apk \
36+
test.robot
37+
```
38+
39+
iOS example:
40+
```
41+
PYTHONPATH=$PYTHONPATH:. robot \
42+
--variable PLATFORM:iOS \
43+
--variable VERSION:17.0 \
44+
--variable DEVICE:iPhone.* \
45+
--variable APP:https://testingbot.com/appium/sample.ipa \
46+
test.robot
47+
```
48+
49+
50+
### Resources
51+
##### [TestingBot Documentation](https://testingbot.com/support/app-automate)
52+
53+
##### [AppiumLibrary Documentation](https://docs.robotframework.org/docs/different_libraries/appium)
54+
55+
##### [RobotFramework Documentation](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html)

TestingBot.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
import requests
3+
from appium import webdriver
4+
from appium.webdriver.client_config import AppiumClientConfig
5+
from robot.libraries.BuiltIn import BuiltIn
6+
7+
HUB_URL = "https://hub.testingbot.com/wd/hub"
8+
API_URL = "https://api.testingbot.com/v1/tests/{session_id}"
9+
10+
11+
def _credentials():
12+
key = os.environ.get("TB_KEY")
13+
secret = os.environ.get("TB_SECRET")
14+
if not key or not secret:
15+
raise RuntimeError("TB_KEY and TB_SECRET environment variables must be set")
16+
return key, secret
17+
18+
19+
def open_testingbot_app(options):
20+
key, secret = _credentials()
21+
client_config = AppiumClientConfig(
22+
remote_server_addr=HUB_URL,
23+
username=key,
24+
password=secret,
25+
)
26+
driver = webdriver.Remote(
27+
command_executor=HUB_URL,
28+
options=options,
29+
client_config=client_config,
30+
)
31+
appium = BuiltIn().get_library_instance("AppiumLibrary")
32+
appium._cache.register(driver, alias="testingbot")
33+
34+
35+
def report_testingbot_status(name, status):
36+
key, secret = _credentials()
37+
appium = BuiltIn().get_library_instance("AppiumLibrary")
38+
session_id = appium._current_application().session_id
39+
payload = {"test[name]": name, "test[success]": int(status == "PASS")}
40+
response = requests.put(
41+
API_URL.format(session_id=session_id),
42+
data=payload,
43+
auth=(key, secret),
44+
timeout=30,
45+
)
46+
assert response.status_code == 200, response.text

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
robotframework
2+
robotframework-appiumlibrary
3+
Appium-Python-Client>=3.0
4+
requests

0 commit comments

Comments
 (0)