Overview
This guide walks you through automating mobile app testing with the noqa API in your CI/CD pipeline. You’ll learn how to:- Get your applications list
- Upload your iOS or Android build
- Retrieve test cases
- Get available devices
- Create and run tests
- Monitor test results
x-api-key header.
For complete API documentation with all available endpoints and parameters, see the API Reference.
Step 1: Get Your Apps
First, retrieve the list of your applications to get theapp_id needed for subsequent steps.
Copy
import requests
API_KEY = "your-api-key"
BASE_URL = "https://api.noqa.ai"
# Get list of apps
response = requests.get(
f"{BASE_URL}/v1/apps/",
headers={"x-api-key": API_KEY}
)
apps = response.json()
# Select first app or find by name
app = apps[0]
APP_ID = app["id"]
print(f"Using app: {app['name']} ({APP_ID})")
Copy
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Mobile App",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"app_context": null
}
]
Step 2: Get Presigned Upload URL
Request a presigned URL to upload your.ipa (iOS) or .apk (Android) build file.
Copy
import requests
API_KEY = "your-api-key"
APP_ID = "550e8400-e29b-41d4-a716-446655440000"
BASE_URL = "https://api.noqa.ai"
FILENAME = "app.ipa" # or "app.apk" for Android
# Get presigned URL
response = requests.post(
f"{BASE_URL}/v1/builds/presigned-url",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"filename": FILENAME
}
)
presigned_data = response.json()
upload_url = presigned_data["upload_url"]
object_key = presigned_data["key"]
expires_in = presigned_data["expires_in"]
print(f"Upload URL expires in {expires_in} seconds")
Copy
{
"upload_url": "https://storage.example.com/...",
"key": "builds/d773ade3-7053-4a91-9dd8-d2e59ba58d0a/550e8400-e29b-41d4-a716-446655440000.ipa",
"expires_in": 3600,
"method": "PUT"
}
Step 2: Upload Build File
Upload your.ipa (iOS) or .apk (Android) file to the presigned URL using PUT request.
Copy
# Upload the build file (app.ipa for iOS or app.apk for Android)
with open("path/to/your/app.ipa", "rb") as f:
upload_response = requests.put(
upload_url,
data=f,
headers={"Content-Type": "application/octet-stream"}
)
if upload_response.status_code == 200:
print("Build uploaded successfully")
else:
print(f"Upload failed: {upload_response.status_code}")
Step 4: Create Build Record
After uploading the file, create a build record in noqa.Copy
# Create build record
build_response = requests.post(
f"{BASE_URL}/v1/builds/",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"key": object_key,
"name": "CI Build #123"
}
)
build_data = build_response.json()
build_id = build_data["id"]
print(f"Build created: {build_id}")
print(f"Version: {build_data['version']}")
Copy
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2024-12-31T10:30:00Z",
"name": "CI Build #123",
"version": "1.0.0 (5)",
"key": "builds/.../550e8400-e29b-41d4-a716-446655440000.ipa",
"file_size": 10485760,
"bundle_id": "com.example.app"
}
Step 5: Get Test Cases
Retrieve the list of test cases for your app.Copy
# Get test cases
cases_response = requests.get(
f"{BASE_URL}/v1/cases/",
headers={"x-api-key": API_KEY},
params={"app_id": APP_ID}
)
cases = cases_response.json()
# Get first test case ID
test_case_id = cases[0]["id"]
print(f"Found {len(cases)} test cases")
print(f"Using test case: {cases[0]['title']} ({test_case_id})")
Copy
[
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Login with valid credentials",
"instructions": "Open app, tap login button, enter valid credentials...",
"tags": ["smoke", "authentication"]
}
]
Step 6: Get Available Devices
Fetch the list of available devices for testing. You can filter by platform (iOS or Android).Copy
# Get available devices (optionally filter by platform)
devices_response = requests.get(
f"{BASE_URL}/v1/devices/",
headers={"x-api-key": API_KEY},
params={"platform": "ios"} # or "android", omit to get all
)
devices = devices_response.json()
# Select first available device
device = devices[0]
print(f"Using device: {device['model']} ({device['platform'].upper()} {device['os_version']})")
Copy
[
{
"model": "iPhone 15 Pro",
"os_version": "17.6.1",
"platform": "ios"
},
{
"model": "iPhone 14",
"os_version": "17.5.0",
"platform": "ios"
},
{
"model": "Google Pixel 8",
"os_version": "14",
"platform": "android"
}
]
Step 7: Create Test Run
Start a new test run with your build, test cases, and selected device.Copy
# Create test run
run_response = requests.post(
f"{BASE_URL}/v1/runs/",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"build_id": build_id,
"cases": {
"ids": [test_case_id]
},
"device": {
"model": device["model"],
"os_version": device["os_version"],
"platform": device["platform"]
}
}
)
run_data = run_response.json()
run_id = run_data["id"]
print(f"Test run started: {run_id}")
print(f"Status: {run_data['status']}")
Copy
{
"id": "789e0123-e89b-12d3-a456-426614174999",
"created_at": "2024-12-31T10:35:00Z",
"status": "queued",
"test_count": 1,
"statuses": {
"queued": 1
}
}
Step 8: Check Run Status
Poll the test run status to monitor progress and get results.Copy
import time
# Poll for test completion
while True:
status_response = requests.get(
f"{BASE_URL}/v1/runs/{run_id}",
headers={"x-api-key": API_KEY},
params={"app_id": APP_ID}
)
run_status = status_response.json()
print(f"Status: {run_status['status']}")
if run_status["status"] in ["passed", "failed", "cancelled"]:
print(f"Tests completed!")
print(f"Total tests: {run_status['test_count']}")
print(f"Statuses: {run_status.get('statuses', {})}")
break
time.sleep(10) # Wait 10 seconds before next check
Copy
{
"id": "789e0123-e89b-12d3-a456-426614174999",
"created_at": "2024-12-31T10:35:00Z",
"status": "passed",
"test_count": 1,
"statuses": {
"passed": 1,
"failed": 0
},
"build": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"version": "1.0.0 (5)",
"name": "CI Build #123"
},
"device": {
"model": "iPhone 15 Pro",
"os_version": "17.6.1",
"platform": "ios",
"locale": "en_US"
},
"tests": [
{
"case_id": "123e4567-e89b-12d3-a456-426614174000",
"case_name": "Login with valid credentials",
"status": "passed"
}
]
}
Run Status Values
The test run can have the following statuses:queued- Test is waiting to startrunning- Test is currently executingpassed- All tests passed successfullyfailed- One or more tests failedcancelled- Test run was cancelled
Selecting Multiple Test Cases
You can run multiple test cases by providing multiple IDs or using tags:Copy
# Run multiple specific tests
run_response = requests.post(
f"{BASE_URL}/v1/runs/",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"build_id": build_id,
"cases": {
"ids": [
"123e4567-e89b-12d3-a456-426614174000",
"456e7890-e89b-12d3-a456-426614174111"
]
},
"device": {
"model": device["model"],
"os_version": device["os_version"],
"platform": device["platform"]
}
}
)
# Or run all tests with specific tags
run_response = requests.post(
f"{BASE_URL}/v1/runs/",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"build_id": build_id,
"cases": {
"tags": ["smoke", "regression"]
},
"device": {
"model": device["model"],
"os_version": device["os_version"],
"platform": device["platform"]
}
}
)
CI/CD Integration Examples
scripts/run_noqa_tests.py:
Copy
#!/usr/bin/env python3
import os
import sys
import time
import json
import requests
from pathlib import Path
# Configuration from environment
API_KEY = os.environ.get("NOQA_API_KEY")
APP_ID = os.environ.get("APP_ID") # Optional - will fetch if not provided
APP_NAME = os.environ.get("APP_NAME") # Optional - app name to search for
BUILD_PATH = os.environ.get("BUILD_PATH", "build/MyApp.ipa") # or .apk for Android
PLATFORM = os.environ.get("PLATFORM", "ios") # "ios" or "android"
BASE_URL = "https://api.noqa.ai"
def log(message):
print(f"[noqa] {message}")
def get_app_id():
"""Get app ID from environment or fetch from API"""
if APP_ID:
log(f"Using APP_ID from environment: {APP_ID}")
return APP_ID
log("Fetching apps list...")
response = requests.get(
f"{BASE_URL}/v1/apps/",
headers={"x-api-key": API_KEY}
)
response.raise_for_status()
apps = response.json()
if not apps:
raise ValueError("No apps found in workspace")
# If APP_NAME is provided, find by name
if APP_NAME:
app = next((a for a in apps if a["name"] == APP_NAME), None)
if not app:
raise ValueError(f"App with name '{APP_NAME}' not found")
log(f"Found app by name: {app['name']} ({app['id']})")
else:
# Use first app
app = apps[0]
log(f"Using first app: {app['name']} ({app['id']})")
return app["id"]
def get_presigned_url():
"""Get presigned URL for build upload"""
log("Requesting presigned upload URL...")
filename = Path(BUILD_PATH).name
response = requests.post(
f"{BASE_URL}/v1/builds/presigned-url",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"filename": filename
}
)
response.raise_for_status()
return response.json()
def upload_build(upload_url, build_path):
"""Upload build file (IPA or APK) to presigned URL"""
log(f"Uploading build from {build_path}...")
file_size = Path(build_path).stat().st_size
log(f"File size: {file_size / 1024 / 1024:.2f} MB")
with open(build_path, "rb") as f:
response = requests.put(
upload_url,
data=f,
headers={"Content-Type": "application/octet-stream"}
)
response.raise_for_status()
log("Build uploaded successfully")
def create_build(object_key):
"""Create build record"""
log("Creating build record...")
response = requests.post(
f"{BASE_URL}/v1/builds/",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"key": object_key,
"name": f"CI Build {os.environ.get('GITHUB_RUN_NUMBER', 'local')}"
}
)
response.raise_for_status()
build_data = response.json()
log(f"Build created: {build_data['id']} (v{build_data['version']})")
return build_data
def get_test_cases():
"""Get available test cases"""
log("Fetching test cases...")
response = requests.get(
f"{BASE_URL}/v1/cases/",
headers={"x-api-key": API_KEY},
params={"app_id": APP_ID, "tags": "smoke"} # Filter by smoke tests
)
response.raise_for_status()
cases = response.json()
log(f"Found {len(cases)} smoke test cases")
return cases
def get_devices():
"""Get available devices"""
log(f"Fetching available {PLATFORM.upper()} devices...")
response = requests.get(
f"{BASE_URL}/v1/devices/",
headers={"x-api-key": API_KEY},
params={"platform": PLATFORM}
)
response.raise_for_status()
devices = response.json()
log(f"Found {len(devices)} available {PLATFORM.upper()} devices")
return devices
def create_run(build_id, case_ids, device):
"""Create test run"""
log("Creating test run...")
response = requests.post(
f"{BASE_URL}/v1/runs/",
headers={"x-api-key": API_KEY},
json={
"app_id": APP_ID,
"build_id": build_id,
"cases": {"ids": case_ids},
"device": {
"model": device["model"],
"os_version": device["os_version"],
"platform": device["platform"],
"locale": "en_US"
}
}
)
response.raise_for_status()
run_data = response.json()
log(f"Test run created: {run_data['id']}")
return run_data
def poll_run_status(run_id, timeout=1800):
"""Poll test run status until completion"""
log(f"Monitoring test run {run_id}...")
start_time = time.time()
while time.time() - start_time < timeout:
response = requests.get(
f"{BASE_URL}/v1/runs/{run_id}",
headers={"x-api-key": API_KEY},
params={"app_id": APP_ID}
)
response.raise_for_status()
run_status = response.json()
status = run_status["status"]
log(f"Status: {status}")
if status in ["passed", "failed", "cancelled"]:
return run_status
time.sleep(15) # Poll every 15 seconds
raise TimeoutError(f"Test run did not complete within {timeout} seconds")
def main():
try:
# Step 1: Get app ID
global APP_ID
APP_ID = get_app_id()
# Step 2: Get presigned URL
presigned_data = get_presigned_url()
# Step 3: Upload build
upload_build(presigned_data["upload_url"], BUILD_PATH)
# Step 4: Create build record
build_data = create_build(presigned_data["key"])
# Step 5: Get test cases
cases = get_test_cases()
if not cases:
log("No test cases found with 'smoke' tag")
return 1
case_ids = [case["id"] for case in cases]
# Step 6: Get devices
devices = get_devices()
if not devices:
log(f"No {PLATFORM.upper()} devices available")
return 1
device = devices[0] # Use first available device
log(f"Using device: {device['model']} ({device['platform'].upper()} {device['os_version']})")
# Step 7: Create and start test run
run_data = create_run(build_data["id"], case_ids, device)
# Step 8: Wait for completion
final_status = poll_run_status(run_data["id"])
# Save results
with open("test-results.json", "w") as f:
json.dump(final_status, f, indent=2)
# Print summary
log("=" * 50)
log(f"Test run completed: {final_status['status'].upper()}")
log(f"Total tests: {final_status['test_count']}")
log(f"Results: {final_status.get('statuses', {})}")
log("=" * 50)
# Exit with appropriate code
if final_status["status"] == "passed":
log("All tests passed! ✓")
return 0
else:
log("Some tests failed! ✗")
return 1
except Exception as e:
log(f"Error: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())
