Skip to content

Repository files navigation

stromcom/http-smoke

Fast, simple and extensible HTTP smoke-testing toolkit for PHP 8.4+.

A fluent DSL for declaring HTTP smoke tests, parallel execution, capture chains between requests, cookie-shared session flows, pluggable variable sources (.env, JSON, OS env, your own), pluggable reporters (console, JSON, Markdown, HTML, GitHub Actions step summary, your own), and a small DI container so any service can be swapped without forking the package.

composer require --dev stromcom/http-smoke
vendor/bin/http-smoke dev

Features

  • PHP 8.4+readonly classes, enums, strict types, no legacy cruft.
  • Fluent DSL — declarative test definitions in plain PHP.
  • Parallel executioncurl_multi-based, concurrency configurable.
  • Session chains — share cookies across a sequence of requests, run sequentially.
  • Capture variablescaptureJsonPath('hash', 'data.thread.hash') then {@hash} in subsequent URLs / bodies / headers.
  • Variable substitution{ENV_VAR} placeholders resolved from .env, smokeHttp.json, OS env, CLI overrides — or any custom VariableSourceInterface.
  • Pluggable reporters — console, JSON (canonical artefact), Markdown, standalone HTML (browsable, with filters and chain context), GitHub step summary; add your own by implementing ReporterInterface.
  • Pluggable HTTP client — default is curl_multi; implement HttpClientInterface to swap in a mock or alternative backend.
  • Retry — per-test or per-group retry on any failure (useful for eventually-consistent endpoints), independent 5xx retry budget.
  • Circuit breaker — per-group maxFailures skips remaining tests once a group is clearly broken.
  • PHPStan level max + strict rules, no docblock noise.

Quick start

1. Install

composer require --dev stromcom/http-smoke

2. Create test definitions

tests/SmokeHttp/api-01-basic.php:

<?php

declare(strict_types=1);

use Stromcom\HttpSmoke\Definition\Suite;

return static function (Suite $suite): void {
    $suite->group('api.public', maxFailures: 3)
        ->baseUrl('{API_BASE_URL}')

        ->get('/ping')
            ->expectStatus(200)
            ->expectJsonPath('status', 'ok')

        ->get('/version')
            ->expectStatus(200)
            ->expectJsonHasKeys(['version', 'commit']);
};

3. Provide variables

Either via .env.dev:

API_BASE_URL=http://localhost:8080/api

…or tests/smokeHttp.json:

{
    "dev":  { "API_BASE_URL": "http://localhost:8080/api" },
    "prod": { "API_BASE_URL": "https://example.com/api" }
}

4. Run

vendor/bin/http-smoke dev
vendor/bin/http-smoke prod --concurrency=20 --output-json=report.json

CLI reference

http-smoke <environment> [options]

  --concurrency=N         Max parallel requests (default: 10)
  --config-dir=DIR        Test definitions directory (default: tests/SmokeHttp)
  --config=FILE           Path to smoke.config.php (default: ./smoke.config.php)
  --smoke-json=FILE       Path to smokeHttp.json (default: ./tests/smokeHttp.json)
  --env-file=FILE         Path to .env.<env> file
  --base-url=URL          Override APP_BASE_URL variable
  --var=KEY=VALUE         Set/override an arbitrary variable (repeatable)
  --filter=PATTERN        Only run files matching *PATTERN*
  --group=NAME            Only run group(s); supports wildcards (api.*)
  --output=FILE           Write Markdown report
  --output-json=FILE      Write canonical JSON report
  --output-html=FILE      Write standalone HTML report
  --no-payloads           Omit request/response payloads
  --payload-limit=BYTES   Truncate each payload body (default: 16384)
  --no-console            Suppress console output
  --no-github-summary     Skip GITHUB_STEP_SUMMARY
  --verbose, -v           Show full request/response detail
  --insecure, -k          Skip TLS verification
  --help, -h

Options take their value either with = or as the next argument:

vendor/bin/http-smoke prod --output-html=report.html
vendor/bin/http-smoke prod --output-html report.html

Unknown options, missing values, non-numeric numbers and stray positional arguments are hard errors (exit code 3) — the runner never silently ignores a flag it does not know, so a typo like --output-htlm= cannot quietly skip your report.

Exit codes: 0 success · 1 tests failed · 2 config error · 3 usage error.


DSL cheat-sheet

$suite->header('Authorization', '{API_TOKEN}'); // suite-level default header
$suite->asJson();                                // suite-level: send all bodies as JSON

$suite->group('api.users', maxFailures: 3)
    ->baseUrl('{API_BASE_URL}')
    ->header('X-Tenant', 'smoke')
    ->defaultTimeout(5)
    ->defaultRetryOnFailure(10, 50)              // up to 10 retries, 50 ms apart
    // ->defaultRetries(10, 50)                  // shorter alias for the above
    ->defaultAsJson()

    // Sessions: shared cookie jar, sequential execution, fail-fast
    ->session('user-lifecycle')
        ->post('/users/', ['email' => 'x@y.z'])
            ->expectStatus(201)
            ->captureJsonPath('userHash', 'data.hash')
        ->get('/users/{@userHash}/')
            ->expectStatus(200)
        ->delete('/users/{@userHash}/')
            ->expectStatus(204)
    ->endSession()

    // Independent (parallel) requests
    ->get('/users/')
        ->expectStatus(200)
        ->expectJson()
        ->expectJsonHasKeys(['data', 'meta.count'])
        ->expectJsonPath('status', 'success')

    ->put('{@externalUploadUrl}', file_get_contents('photo.jpg'))
        ->noGroupHeaders()                       // skip group Authorization header
        ->asJson(false)                          // raw body
        ->expectStatus(200)

    ->get('/dashboard')
        ->expectStatus(200)
        ->expectHtmlElement('h1', 'Dashboard')         // <h1>Dashboard</h1>
        ->expectHtmlElement('a', null, 'href', '/logout')
        ->expectHtmlElement('meta', null, 'name', 'viewport')

    ->get('/health')
        ->expectHeaderContains('Cache-Control', 'no-store')
        ->expect(fn (Stromcom\HttpSmoke\Http\Response $r) =>
            json_decode($r->body, true)['queue'] > 0 ? 'queue should be empty' : null
        );

Available expectations

Method Purpose
expectStatus(int) exact status code
expectStatusOneOf(int, ...) one of several status codes
expectRedirect(string) 3xx with Location header path-matching the URL
expectContains(string) / expectNotContains(string) body substring
expectJson() body must parse as JSON
expectJsonHasKeys(array) dot-notation paths must exist
expectJsonPath(string, mixed) dot-notation path equals value
expectHtmlElement(tag, text?, attribute?, attributeValue?) HTML body must contain a matching <tag> (optionally with given attribute / text)
expectHeaderContains(string, string) header value contains substring
expect(Closure) custom callback returning null on success or a failure message

Captures

Use captureJsonPath('name', 'data.x.y') (or captureHeader('name', 'X-Foo')) on any request, then reference {@name} in any later request's URL, body, or header value.


Configuration files

The package merges configuration from several layers (later wins):

  1. Defaults
  2. smoke.config.php (PHP — for registering custom services / closures)
  3. smokeHttp.json (per-environment static values)
  4. .env.<environment>
  5. OS environment variables (getenv())
  6. CLI options (--base-url, --var=KEY=VALUE, …)

smokeHttp.json

{
    "dev":     { "API_BASE_URL": "http://localhost:8080/api" },
    "staging": { "API_BASE_URL": "https://staging.example.com/api" },
    "prod":    { "API_BASE_URL": "https://example.com/api" }
}

smoke.config.php

<?php

declare(strict_types=1);

use Stromcom\HttpSmoke\Config\SmokeConfig;
use Stromcom\HttpSmoke\Container\Container;
use Stromcom\HttpSmoke\Variable\Source\ArraySource;

return static function (SmokeConfig $config): void {
    $config->configDir          = __DIR__ . '/tests/SmokeHttp';
    $config->concurrency        = 10;
    $config->jsonOutputPath     = __DIR__ . '/build/smoke.json';
    $config->markdownOutputPath = __DIR__ . '/build/smoke.md';
    $config->htmlOutputPath     = __DIR__ . '/build/smoke.html';

    // Plug in additional variable sources
    $config->extraVariableSources[] = new ArraySource([
        'CUSTOM_KEY' => 'value',
    ]);

    // Override services in the DI container
    $config->configureContainer = function (Container $container): void {
        // $container->set(HttpClientInterface::class, fn() => new MyClient());
    };
};

Extending

Custom variable source

Implement Stromcom\HttpSmoke\Variable\VariableSourceInterface:

final class AwsSecretsSource implements VariableSourceInterface
{
    public function get(string $name): ?string { /* ... */ }
    public function all(): array { /* ... */ }
}

// In smoke.config.php:
$config->extraVariableSources[] = new AwsSecretsSource(...);

Custom reporter

Implement Stromcom\HttpSmoke\Reporting\ReporterInterface (onStart, onResult, onEnd) and add it via $config->extraReporters[] = new MyReporter().

Custom HTTP client

Implement Stromcom\HttpSmoke\Http\HttpClientInterface and register in the container via $config->configureContainer.


JSON report schema

The JSON report is the canonical machine-readable artefact. Markdown, HTML and GitHub step-summary outputs are derived from it.

{
  "meta": {
    "schema_version": 3,
    "environment": "prod",
    "generated_at": "2026-04-26T12:00:00+00:00",
    "duration_s": 4.521,
    "concurrency": 10,
    "base_path": "/var/www/app",
    "payloads": true,
    "summary": { "total": 15, "passed": 13, "failed": 1, "skipped": 1, "success": false }
  },
  "groups": [
    {
      "name": "api.users",
      "summary": { "total": 5, "passed": 4, "failed": 1, "skipped": 0 },
      "tests": [
        {
          "label": "POST /users/ – create",
          "method": "POST",
          "url": "https://example.com/api/users/",
          "status": "failed",
          "http_code": 500,
          "duration_ms": 234,
          "attempts": 1,
          "total_duration_ms": 234,
          "session": { "label": "user lifecycle" },
          "source": {
            "file": "tests/SmokeHttp/api/users.php",
            "absolute_path": "/var/www/app/tests/SmokeHttp/api/users.php",
            "line": 24
          },
          "failures": ["Expected status 201, got 500"],
          "skip_reason": null,
          "chain_context": [/* preceding session steps */],
          "request": {
            "method": "POST",
            "url": "https://example.com/api/users/",
            "query": null,
            "headers": { "Authorization": "«redacted»", "Content-Type": "application/json" },
            "body_encoding": "json",
            "body": { "text": "{\"name\":\"Ada\"}", "size_bytes": 14, "truncated": false, "binary": false },
            "timeout_s": 10,
            "cookie_jar": true,
            "insecure_tls": false,
            "user_agent": "StromcomSmokeTest/1.0"
          },
          "response": {
            "status_code": 500,
            "headers": { "content-type": "application/json" },
            "content_type": "application/json",
            "body": { "text": "{\"error\":\"boom\"}", "size_bytes": 16, "truncated": false, "binary": false },
            "transport_error": null
          }
        }
      ]
    }
  ]
}

HTML report

vendor/bin/http-smoke staging --output-html=build/smoke.html

One self-contained file — no CDN, no assets, no server. Open it in a browser or publish it as a CI artefact. It embeds the canonical JSON and renders:

  • summary cards + pass/fail/skip ratio bar, environment / duration / concurrency meta,
  • groups with per-group tallies; failed tests are expanded by default,
  • per-test detail: method, URL, HTTP code, duration, attempts (total time incl. retries),
  • all failure messages verbatim, skip reasons,
  • payloads — the request exactly as sent (headers incl. the implicit Content-Type, query parameters, body in its wire encoding) and the response (headers, pretty-printed body), plus a ready-to-run cURL reproduction,
  • source — the definition file and line of each test (tests/SmokeHttp/api/users.php:24), as plain selectable text,
  • chain context for session tests — every preceding step of the session (method, status, label, URL, duration) with the failing step highlighted,
  • filters: full-text search (label / URL / failure message / payload / path), status (all / failed / passed / skipped), group, definition file, retried-only, sessions-only, plus expand/collapse all and “show payloads”.

Dark mode follows the OS setting. The layout logic lives in src/Reporting/templates/report.html; HtmlReporter only injects the JSON.

Payloads and secrets

Payloads land in both the JSON and the HTML report, so treat those artefacts as sensitive. Sensitive headers (Authorization, Cookie, Set-Cookie, X-Api-Key, …) are replaced with «redacted», bodies are truncated at 16 kB and binary bodies are reported as size only.

vendor/bin/http-smoke prod --output-html=report.html --no-payloads
vendor/bin/http-smoke prod --output-html=report.html --payload-limit=65536
$config->includePayloads = false;                 // drop request/response blocks entirely
$config->payloadMaxBytes = 65536;                 // per-body cap
$config->redactedHeaders = ['authorization', 'x-tenant-secret'];

Examples

See examples/ for ready-to-run test suites and a sample smoke.config.php / smokeHttp.json.


License

MIT — see LICENSE.

About

PHP 8.4+ HTTP smoke-testing library with a fluent DSL, parallel execution, and pluggable reporters. For post-deploy checks, monitoring, and CI gates.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages