Skip to main content

Overview

This guide walks you through automating mobile app testing with the noqa API in your CI/CD pipeline. You’ll learn how to:
  1. Get your applications list
  2. Upload your iOS or Android build
  3. Retrieve test cases
  4. Get available devices
  5. Create and run tests
  6. Monitor test results
All API endpoints require authentication using an API key passed in the 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 the app_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})")
Response:
[
  {
    "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")
Response:
{
  "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}")

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']}")
Response:
{
  "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})")
Response:
[
  {
    "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']})")
Response:
[
  {
    "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']}")
Response:
{
  "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
Response (completed run):
{
  "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 start
  • running - Test is currently executing
  • passed - All tests passed successfully
  • failed - One or more tests failed
  • cancelled - 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"]
        }
    }
)

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())