Skip to content
Back to skills

Api Testing

ASecurity

Use when writing API endpoint tests — integration tests, contract validation, response assertions, mocked external services — even when the user says 'test this route' without naming API testing.

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added June 5, 2026
ai-agentsgophpshellbashtestingapidatabase

Works with

  • api

Security analysis

A100/100

Scanned June 5, 2026

npx -y skills add event4u-app/agent-config --skill api-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Api Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Api Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/event4u-app-api-testing/badge)](https://www.skillsdirectory.com/skills/event4u-app-api-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
model_tier: medium
name: api-testing
description: "Use when writing API endpoint tests — integration tests, contract validation, response assertions, mocked external services — even when the user says 'test this route' without naming API testing."
domain: quality
workspaces:
  - engineering
packs:
  - engineering-base
---

# api-testing

## When to use

Use this skill when writing or reviewing API endpoint tests — integration tests,
contract validation, response structure checks, or external service mocking.

## Procedure: Write API tests

1. **Understand the endpoint** — Read the controller, form request, and existing tests. Understand expected behavior, edge cases, and auth requirements before writing anything.
2. **Set up test data** — Use seeders (preferred) or factories. Mock external services with `Http::fake()`.
3. **Write test cases** — Cover success, validation errors, authorization failures, edge cases.
4. **Assert response** — Check status code, JSON structure, data values. Use `assertJsonStructure()`.
5. **Verify** — Run the test. Must pass. Check no flaky assertions (no time-dependent, no random ordering).

### Example

```php
describe('GET /api/v1/projects', function () {
    it('returns paginated projects for authenticated user', function () {
        $user = loginAsTestUser();

        $response = $this->getJson('/api/v1/projects');

        $response->assertOk()
            ->assertJsonStructure([
                'data' => [['id', 'title', 'status']],
                'meta' => ['current_page', 'per_page', 'total'],
            ]);
    });

    it('returns 401 for unauthenticated request', function () {
        $this->getJson('/api/v1/projects')
            ->assertUnauthorized();
    });

    it('returns 403 when user lacks permission', function () {
        loginAsRestrictedUser();

        $this->getJson('/api/v1/projects')
            ->assertForbidden();
    });
});
```

## Test categories

### Happy path

Test the expected success scenario with valid input:

```php
it('creates a project', function () {
    loginAsTestUser();

    $this->postJson('/api/v1/projects', [
        'title' => 'New Project',
        'customer_id' => $customerId,
    ])
        ->assertCreated()
        ->assertJsonPath('data.title', 'New Project');

    $this->assertDatabaseHas('projects', ['title' => 'New Project']);
});
```

### Validation

Test that invalid input is rejected with correct error messages:

```php
it('rejects project without title', function () {
    loginAsTestUser();

    $this->postJson('/api/v1/projects', [
        'customer_id' => $customerId,
    ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['title']);
});
```

### Authorization

Test that unauthorized access is blocked:

```php
it('prevents non-owner from updating project', function () {
    $otherUser = loginAsOtherUser();

    $this->putJson("/api/v1/projects/{$project->id}", [
        'title' => 'Hijacked',
    ])
        ->assertForbidden();
});
```

### Edge cases

Test boundary conditions:

```php
it('handles empty collection', function () {
    loginAsTestUser();

    $this->getJson('/api/v1/projects')
        ->assertOk()
        ->assertJsonCount(0, 'data');
});

it('paginates large result sets', function () {
    loginAsTestUser();

    $this->getJson('/api/v1/projects?per_page=5')
        ->assertOk()
        ->assertJsonPath('meta.per_page', 5);
});
```

## Response contract validation

### Assert JSON structure

```php
// Verify response shape (keys exist)
$response->assertJsonStructure([
    'data' => ['id', 'title', 'status', 'created_at'],
]);

// Verify exact values
$response->assertJsonPath('data.status', 'active');

// Verify collection count
$response->assertJsonCount(3, 'data');
```

### Assert response types

```php
// When strict typing matters
$data = $response->json('data');
expect($data['id'])->toBeInt();
expect($data['title'])->toBeString();
expect($data['total'])->toBeString(); // Money as string, not float
```

### Filter noisy responses

When a failing test dumps the full JSON body, narrow the diagnosis with `jq` or `grep`
instead of scrolling the whole payload:

```bash
# Extract only the failing assertion path
echo "$RESPONSE_JSON" | jq '.data.status, .errors'

# Targeted log scan
rg --json 'API call failed' storage/logs/laravel.log | jq -r '.data.lines.text'
```

## External service mocking

```php
it('handles external API failure gracefully', function () {
    Http::fake([
        'external-api.com/*' => Http::response(null, 500),
    ]);

    loginAsTestUser();

    $this->postJson('/api/v1/sync')
        ->assertStatus(502)
        ->assertJsonPath('message', 'External service unavailable');
});
```

## Test checklist per endpoint

| Category | Tests needed |
|---|---|
| **Auth** | Unauthenticated (401), unauthorized (403) |
| **Validation** | Missing fields, wrong types, boundary values |
| **Happy path** | Success with valid input, correct status code |
| **Response** | JSON structure, field types, pagination meta |
| **Side effects** | Database changes, events dispatched, jobs queued |
| **Edge cases** | Empty results, large payloads, concurrent access |

## Bridge to UI verification

API tests cover the contract layer. When an endpoint feeds a UI surface (Livewire component, Blade-rendered page, SPA route), complement the API test with a thin UI probe: a `livewire test` for wired components, or a Playwright spec / browser `screenshot` for the rendered shell. Never assume the UI works just because the API test is green.

## Output format

1. Pest test file covering happy path, validation, auth, and edge cases
2. Test names as readable sentences describing expected behavior
3. Mocked external services where applicable

## Auto-trigger keywords

- API test
- endpoint test
- integration test
- response validation
- contract testing

## Gotcha

- Don't test framework internals (e.g., "does Laravel return 422 on validation error") — test YOUR validation rules.
- Always seed test data explicitly — don't rely on data from other tests (parallel execution).
- Mock external APIs with `Http::fake()` — never hit real services in tests.
- The model forgets to assert response structure, only checking status codes — always check both.

## Do NOT

- Do not hardcode IDs or timestamps — use factories or seeders.
- Do not skip auth tests — always test both authenticated and unauthenticated.
- Do not assert entire JSON responses — assert only meaningful fields.
- Do not use `Http::fake()` without also testing the real integration path.

## Anti-bruteforce — diagnose before retry

When a test fails, do not retry blindly with tweaked assertions until something passes. Diagnose the root cause first: print the actual response shape once, compare it to the contract, then write a targeted fix. Trial-and-error retries hide real regressions.

## Clarification guard — ambiguous contract → ask

If the endpoint contract is ambiguous (unclear status code, optional fields, error envelope shape), do not assume. Ask the user or check the OpenAPI spec / route definition before writing assertions — never guess the response shape from the route name.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…