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.
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})")
const API_KEY = "your-api-key";
const BASE_URL = "https://api.noqa.ai";
// Get list of apps
const appsResponse = await fetch(`${BASE_URL}/v1/apps/`, {
headers: {
"x-api-key": API_KEY,
},
});
const apps = await appsResponse.json();
// Select first app or find by name
const app = apps[0];
const APP_ID = app.id;
console.log(`Using app: ${app.name} (${APP_ID})`);
curl -X GET "https://api.noqa.ai/v1/apps/" \
-H "x-api-key: your-api-key"
[
{
"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.
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")
const API_KEY = "your-api-key";
const APP_ID = "550e8400-e29b-41d4-a716-446655440000";
const BASE_URL = "https://api.noqa.ai";
const FILENAME = "app.ipa"; // or "app.apk" for Android
// Get presigned URL
const presignedResponse = await fetch(
`${BASE_URL}/v1/builds/presigned-url`,
{
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: APP_ID,
filename: FILENAME
}),
}
);
const presignedData = await presignedResponse.json();
const uploadUrl = presignedData.upload_url;
const objectKey = presignedData.key;
console.log(`Upload URL expires in ${presignedData.expires_in} seconds`);
curl -X POST "https://api.noqa.ai/v1/builds/presigned-url" \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"app_id": "550e8400-e29b-41d4-a716-446655440000",
"filename": "app.ipa"
}'
{
"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.
# 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}")
// Upload the build file (app.ipa for iOS or app.apk for Android)
const fileBuffer = await fs.readFile("path/to/your/app.ipa");
const uploadResponse = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "application/octet-stream",
},
body: fileBuffer,
});
if (uploadResponse.ok) {
console.log("Build uploaded successfully");
} else {
console.error(`Upload failed: ${uploadResponse.status}`);
}
curl -X PUT "https://storage.example.com/..." \
-H "Content-Type: application/octet-stream" \
--data-binary "@path/to/your/app.ipa"
Step 4: Create Build Record
After uploading the file, create a build record in noqa.# 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']}")
// Create build record
const buildResponse = await fetch(`${BASE_URL}/v1/builds/`, {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: APP_ID,
key: objectKey,
name: "CI Build #123",
}),
});
const buildData = await buildResponse.json();
const buildId = buildData.id;
console.log(`Build created: ${buildId}`);
console.log(`Version: ${buildData.version}`);
curl -X POST "https://api.noqa.ai/v1/builds/" \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"app_id": "550e8400-e29b-41d4-a716-446655440000",
"key": "builds/d773ade3-7053-4a91-9dd8-d2e59ba58d0a/550e8400-e29b-41d4-a716-446655440000.ipa",
"name": "CI Build #123"
}'
{
"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.# 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})")
// Get test cases
const casesResponse = await fetch(
`${BASE_URL}/v1/cases/?app_id=${APP_ID}`,
{
headers: {
"x-api-key": API_KEY,
},
}
);
const cases = await casesResponse.json();
const testCaseId = cases[0].id;
console.log(`Found ${cases.length} test cases`);
console.log(`Using test case: ${cases[0].title} (${testCaseId})`);
curl -X GET "https://api.noqa.ai/v1/cases/?app_id=550e8400-e29b-41d4-a716-446655440000" \
-H "x-api-key: your-api-key"
[
{
"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).# 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']})")
// Get available devices (optionally filter by platform)
const devicesResponse = await fetch(
`${BASE_URL}/v1/devices/?platform=ios`, // or "android", omit to get all
{
headers: {
"x-api-key": API_KEY,
},
}
);
const devices = await devicesResponse.json();
const device = devices[0];
console.log(`Using device: ${device.model} (${device.platform.toUpperCase()} ${device.os_version})`);
# Get iOS devices
curl -X GET "https://api.noqa.ai/v1/devices/?platform=ios" \
-H "x-api-key: your-api-key"
# Get Android devices
curl -X GET "https://api.noqa.ai/v1/devices/?platform=android" \
-H "x-api-key: your-api-key"
# Get all devices
curl -X GET "https://api.noqa.ai/v1/devices/" \
-H "x-api-key: your-api-key"
[
{
"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.# 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']}")
// Create test run
const runResponse = await fetch(`${BASE_URL}/v1/runs/`, {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: APP_ID,
build_id: buildId,
cases: {
ids: [testCaseId],
},
device: {
model: device.model,
os_version: device.os_version,
platform: device.platform,
},
}),
});
const runData = await runResponse.json();
const runId = runData.id;
console.log(`Test run started: ${runId}`);
console.log(`Status: ${runData.status}`);
curl -X POST "https://api.noqa.ai/v1/runs/" \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"app_id": "550e8400-e29b-41d4-a716-446655440000",
"build_id": "550e8400-e29b-41d4-a716-446655440000",
"cases": {
"ids": ["123e4567-e89b-12d3-a456-426614174000"]
},
"device": {
"model": "iPhone 15 Pro",
"os_version": "17.6.1",
"platform": "ios"
}
}'
{
"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.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
// Poll for test completion
async function pollRunStatus() {
while (true) {
const statusResponse = await fetch(
`${BASE_URL}/v1/runs/${runId}?app_id=${APP_ID}`,
{
headers: {
"x-api-key": API_KEY,
},
}
);
const runStatus = await statusResponse.json();
console.log(`Status: ${runStatus.status}`);
if (["passed", "failed", "cancelled"].includes(runStatus.status)) {
console.log("Tests completed!");
console.log(`Total tests: ${runStatus.test_count}`);
console.log(`Statuses:`, runStatus.statuses);
break;
}
// Wait 10 seconds before next check
await new Promise((resolve) => setTimeout(resolve, 10000));
}
}
await pollRunStatus();
# Check status (run this periodically)
curl -X GET "https://api.noqa.ai/v1/runs/789e0123-e89b-12d3-a456-426614174999?app_id=550e8400-e29b-41d4-a716-446655440000" \
-H "x-api-key: your-api-key"
{
"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:# 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"]
}
}
)
// Run multiple specific tests
const runResponse = await fetch(`${BASE_URL}/v1/runs/`, {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: APP_ID,
build_id: buildId,
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
const tagRunResponse = await fetch(`${BASE_URL}/v1/runs/`, {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: APP_ID,
build_id: buildId,
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:
#!/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())
