Skip to content

Mock AWX/AAP Testing Guide#

This guide shows you how to test AWX/Ansible Automation Platform API integrations without a real AWX server using mocked HTTP responses.

Why Mock AWX Testing?#

  • No Infrastructure Required: Test without deploying AWX/Tower
  • Fast Execution: Tests run in milliseconds
  • Reliable: No network dependencies or authentication complexity
  • Controlled Scenarios: Test edge cases, errors, and specific API responses
  • CI/CD Friendly: Run in any environment without AWX infrastructure

Quick Start#

Run All Mock Tests#

# Run all AWX mock API tests
poetry run pytest tests/integration/test_awx_mock.py -v

# Run with specific test class
poetry run pytest tests/integration/test_awx_mock.py::TestAWXMockJobTemplates -v

# Run single test
poetry run pytest tests/integration/test_awx_mock.py::TestAWXMockJobs::test_get_job_status -v

Example Output#

tests/integration/test_awx_mock.py::TestAWXMockJobTemplates::test_list_job_templates PASSED
tests/integration/test_awx_mock.py::TestAWXMockJobTemplates::test_create_job_template PASSED
tests/integration/test_awx_mock.py::TestAWXMockJobTemplates::test_launch_job_template PASSED
tests/integration/test_awx_mock.py::TestAWXMockInventories::test_list_inventories PASSED
...
======================== 20 passed in 1.2s ========================

AWX API Coverage#

The mock tests cover all major AWX/AAP API endpoints:

[YES] Job Templates#

  • List job templates
  • Create job template
  • Launch job template
  • Get job template details

[YES] Inventories#

  • List inventories
  • Sync inventory sources
  • Get inventory hosts and groups

[YES] Projects#

  • List projects
  • Update project (SCM sync)
  • Get project details

[YES] Credentials#

  • List credentials
  • Create credentials
  • Manage credential types

[YES] Workflows#

  • List workflow templates
  • Launch workflow
  • Get workflow job status

[YES] Jobs & Monitoring#

  • Get job status
  • Get job output/stdout
  • Cancel running jobs
  • List job events

[YES] Authentication & Errors#

  • 401 Unauthorized responses
  • 403 Forbidden responses
  • 404 Not Found responses
  • 400 Validation errors

[YES] Organizations#

  • List organizations
  • Get organization details

How It Works#

Mock AWX testing uses the responses library to intercept HTTP requests:

import responses
import requests

@responses.activate
def test_awx_job_template():
    # Mock AWX API response
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/job_templates/",
        json={
            "count": 1,
            "results": [
                {
                    "id": 1,
                    "name": "Deploy Apache",
                    "playbook": "apache_deploy.yml"
                }
            ]
        },
        status=200
    )

    # Make real HTTP request (intercepted by responses)
    response = requests.get(
        "https://awx.example.com/api/v2/job_templates/",
        headers={"Authorization": "Bearer test-token"}
    )

    # Verify response
    assert response.status_code == 200
    assert response.json()["count"] == 1

Writing Your Own AWX Mock Tests#

Basic Structure#

import responses
import requests

@responses.activate
def test_my_awx_feature():
    # 1. Setup mock AWX response
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={
            "job": 42,
            "status": "pending"
        },
        status=201
    )

    # 2. Make API call
    response = requests.post(
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={"extra_vars": {"env": "prod"}},
        headers={"Authorization": "Bearer test-token"}
    )

    # 3. Assert results
    assert response.status_code == 201
    assert response.json()["job"] == 42

Testing Job Launches#

@responses.activate
def test_launch_and_monitor_job():
    # Mock job launch
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={"job": 42, "status": "pending"},
        status=201
    )

    # Mock job status check
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/jobs/42/",
        json={"id": 42, "status": "successful", "failed": False},
        status=200
    )

    # Launch job
    launch_response = requests.post(
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        headers={"Authorization": "Bearer test-token"}
    )
    job_id = launch_response.json()["job"]

    # Check job status
    status_response = requests.get(
        f"https://awx.example.com/api/v2/jobs/{job_id}/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert status_response.json()["status"] == "successful"

Testing Workflows#

@responses.activate
def test_workflow_execution():
    # Mock workflow launch
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/workflow_job_templates/1/launch/",
        json={"workflow_job": 25, "status": "pending"},
        status=201
    )

    # Mock workflow status
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/workflow_jobs/25/",
        json={
            "id": 25,
            "status": "successful",
            "workflow_nodes": [
                {"job": 42, "status": "successful"},
                {"job": 43, "status": "successful"}
            ]
        },
        status=200
    )

    # Launch workflow
    response = requests.post(
        "https://awx.example.com/api/v2/workflow_job_templates/1/launch/",
        headers={"Authorization": "Bearer test-token"}
    )

    workflow_job_id = response.json()["workflow_job"]

    # Check workflow status
    status = requests.get(
        f"https://awx.example.com/api/v2/workflow_jobs/{workflow_job_id}/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert status.json()["status"] == "successful"

Testing Inventory Updates#

@responses.activate
def test_inventory_sync():
    # Mock inventory source sync
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/inventory_sources/5/update/",
        json={"inventory_update": 15, "status": "pending"},
        status=202
    )

    # Mock inventory update status
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/inventory_updates/15/",
        json={"id": 15, "status": "successful", "total_hosts": 50},
        status=200
    )

    # Trigger sync
    sync_response = requests.post(
        "https://awx.example.com/api/v2/inventory_sources/5/update/",
        headers={"Authorization": "Bearer test-token"}
    )

    update_id = sync_response.json()["inventory_update"]

    # Check sync status
    status = requests.get(
        f"https://awx.example.com/api/v2/inventory_updates/{update_id}/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert status.json()["total_hosts"] == 50

Common AWX API Patterns#

List Resources (GET with Pagination)#

responses.add(
    responses.GET,
    "https://awx.example.com/api/v2/job_templates/",
    json={
        "count": 100,
        "next": "/api/v2/job_templates/?page=2",
        "previous": None,
        "results": [
            {"id": 1, "name": "Template 1"},
            {"id": 2, "name": "Template 2"}
        ]
    },
    status=200
)

Create Resource (POST)#

responses.add(
    responses.POST,
    "https://awx.example.com/api/v2/job_templates/",
    json={
        "id": 3,
        "name": "New Template",
        "created": "2026-02-16T12:00:00Z"
    },
    status=201
)

Update Resource (PATCH/PUT)#

responses.add(
    responses.PATCH,
    "https://awx.example.com/api/v2/job_templates/1/",
    json={
        "id": 1,
        "name": "Updated Template",
        "modified": "2026-02-16T12:00:00Z"
    },
    status=200
)

Delete Resource (DELETE)#

responses.add(
    responses.DELETE,
    "https://awx.example.com/api/v2/job_templates/1/",
    status=204  # No content
)

Authentication Errors#

responses.add(
    responses.GET,
    "https://awx.example.com/api/v2/job_templates/",
    json={"detail": "Authentication credentials were not provided."},
    status=401
)

AWX API Authentication Methods#

AWX supports multiple authentication methods:

headers = {"Authorization": "Bearer test-token"}

Basic Auth#

import base64

auth = base64.b64encode(b"username:password").decode()
headers = {"Authorization": f"Basic {auth}"}

Session Auth (OAuth2)#

# Mock OAuth token endpoint
responses.add(
    responses.POST,
    "https://awx.example.com/api/o/token/",
    json={"access_token": "test-token", "expires_in": 36000},
    status=200
)

Testing Error Scenarios#

Validation Errors#

@responses.activate
def test_invalid_job_template():
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/",
        json={
            "name": ["This field is required."],
            "inventory": ["Invalid inventory ID."]
        },
        status=400
    )

    response = requests.post(
        "https://awx.example.com/api/v2/job_templates/",
        json={"description": "Missing required fields"},
        headers={"Authorization": "Bearer test-token"}
    )

    assert response.status_code == 400
    assert "name" in response.json()

Permission Errors#

@responses.activate
def test_insufficient_permissions():
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={"detail": "You do not have permission to perform this action."},
        status=403
    )

    response = requests.post(
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        headers={"Authorization": "Bearer limited-token"}
    )

    assert response.status_code == 403

Resource Not Found#

@responses.activate
def test_template_not_found():
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/job_templates/999/",
        json={"detail": "Not found."},
        status=404
    )

    response = requests.get(
        "https://awx.example.com/api/v2/job_templates/999/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert response.status_code == 404

Debugging Mock Tests#

Inspect Requests Made#

@responses.activate
def test_with_debugging():
    responses.add(responses.GET, "https://awx.example.com/api/v2/jobs/42/", json={}, status=200)

    requests.get("https://awx.example.com/api/v2/jobs/42/", headers={"Authorization": "Bearer test"})

    # Debug: see what was sent
    print(f"Total requests: {len(responses.calls)}")
    print(f"URL: {responses.calls[0].request.url}")
    print(f"Headers: {dict(responses.calls[0].request.headers)}")

Verify Query Parameters#

@responses.activate
def test_with_query_params():
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/job_templates/",
        json={"results": []},
        status=200,
        match=[responses.matchers.query_param_matcher({"page": "2", "page_size": "20"})]
    )

    # This will only match if query params are exactly as specified
    response = requests.get(
        "https://awx.example.com/api/v2/job_templates/",
        params={"page": 2, "page_size": 20}
    )

Performance#

Mock AWX tests are extremely fast:

# 20 comprehensive AWX API tests in ~1.2 seconds
poetry run pytest tests/integration/test_awx_mock.py -v
# 20 passed in 1.20s

Compare to real AWX API calls which might take 1-3 seconds each due to: - Network latency - TLS handshake - Authentication - Database queries - Job scheduling

CI/CD Integration#

<Mock tests work perfectly in CI/CD:

# .github/workflows/test.yml
- name: Run AWX Mock Tests
  run: poetry run pytest tests/integration/test_awx_mock.py -v

No AWX infrastructure needed!

Combining with Chef Server Mocks#

Test complete migration scenarios by mocking both Chef Server AND AWX:

@responses.activate
def test_chef_to_awx_migration():
    # Mock Chef Server node query
    responses.add(
        responses.GET,
        "https://chef.example.com/organizations/default/search/node",
        json={"rows": [{"name": "web-01", "platform": "ubuntu"}]},
        status=200
    )

    # Mock AWX inventory creation
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/inventories/",
        json={"id": 5, "name": "Chef Migrated Hosts"},
        status=201
    )

    # Mock AWX host creation
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/inventories/5/hosts/",
        json={"id": 10, "name": "web-01"},
        status=201
    )

    # Your migration code here...
    # 1. Query Chef Server for nodes
    # 2. Create AWX inventory
    # 3. Populate AWX inventory with nodes

Real AWX Testing#

For testing with a real AWX instance, see: - AWX Official Docs - AWX CLI - AWX API Guide

Further Reading#

  • AWX API Documentation: https://docs.ansible.com/ansible-tower/latest/html/towerapi/
  • responses library: https://github.com/getsentry/responses
  • Mock tests: tests/integration/test_awx_mock.py (repository source)
  • Chef Server mocks: MOCK_CHEF_SERVER.md