0

HTTP testing helpers — get(), post(), assertStatus()

Intermediate5 min read·fw-11-002

Concept

HTTP testing helpersget(), post(), assertStatus() — let tests make fake HTTP requests through the full application stack without a real web server. The response is captured and assertions are made on it.

How it works:

  1. Build a Request object from the test parameters.
  2. Pass it through the application's HTTP kernel (middleware + router + controller).
  3. Capture the Response object.
  4. Return a TestResponse wrapper that provides assertion methods.

TestResponse wrapper: Wraps the framework's Response with assertion methods:

  • assertStatus(int $code): Assert the HTTP status code.
  • assertOk(): Assert 200.
  • assertJson(array $expected): Assert the response body contains this JSON (subset match).
  • assertJsonFragment(array $data): Assert the JSON contains this fragment anywhere.
  • assertSee(string $text): Assert the response body contains this text.
  • assertRedirect(?string $uri = null): Assert the response is a 3xx redirect, optionally to a specific URI.

actingAs($user): Set the authenticated user before making a request. Sets a flag on the test kernel to inject the user into the auth system.

Request building: $this->get('/url') creates a GET Request. $this->post('/url', ['key' => 'val']) creates a POST request with the given body. $this->getJson('/api/url') adds Accept: application/json.

Headers in tests: $this->withHeaders(['Authorization' => "Bearer {$token}"]) — chainable headers added to the next request.

Code Example

php
<?php
namespace Framework\Testing;

abstract class TestCase extends \PHPUnit\Framework\TestCase
{
    private array $defaultHeaders = [];
    private ?object $actingAsUser = null;

    // HTTP testing methods
    protected function get(string $uri, array $headers = []): TestResponse
    {
        return $this->call('GET', $uri, [], $headers);
    }

    protected function post(string $uri, array $data = [], array $headers = []): TestResponse
    {
        return $this->call('POST', $uri, $data, $headers);
    }

    protected function put(string $uri, array $data = [], array $headers = []): TestResponse
    {
        return $this->call('PUT', $uri, $data, $headers);
    }

    protected function patch(string $uri, array $data = [], array $headers = []): TestResponse
    {
        return $this->call('PATCH', $uri, $data, $headers);
    }

    protected function delete(string $uri, array $headers = []): TestResponse
    {
        return $this->call('DELETE', $uri, [], $headers);
    }

    protected function getJson(string $uri, array $headers = []): TestResponse
    {
        return $this->get($uri, array_merge(['Accept' => 'application/json'], $headers));
    }

    protected function postJson(string $uri, array $data = [], array $headers = []): TestResponse
    {
        return $this->post($uri, $data, array_merge([
            'Content-Type' => 'application/json',
            'Accept'       => 'application/json',
        ], $headers));
    }

    protected function actingAs(object $user): static
    {
        $this->actingAsUser = $user;
        return $this;
    }

    protected function withHeaders(array $headers): static
    {
        $this->defaultHeaders = array_merge($this->defaultHeaders, $headers);
        return $this;
    }

    private function call(string $method, string $uri, array $data = [], array $headers = []): TestResponse
    {
        $request = $this->buildRequest($method, $uri, $data, array_merge($this->defaultHeaders, $headers));

        if ($this->actingAsUser !== null) {
            // Bypass auth for this request by pre-injecting the user
            static::$app->instance('test.user', $this->actingAsUser);
            $this->actingAsUser = null;
        }

        $response = static::$app->make(\Framework\Http\Kernel::class)->handle($request);
        return new TestResponse($response);
    }

    private function buildRequest(string $method, string $uri, array $data, array $headers): \Framework\Http\Request
    {
        $body = in_array($method, ['POST', 'PUT', 'PATCH'])
            ? (isset($headers['Content-Type']) && $headers['Content-Type'] === 'application/json'
                ? json_encode($data)
                : http_build_query($data))
            : '';

        return \Framework\Http\Request::create($method, $uri, $headers, $body);
    }
}

class TestResponse
{
    public function __construct(private readonly \Framework\Http\Response $response) {}

    public function assertStatus(int $code): static
    {
        \PHPUnit\Framework\Assert::assertEquals($code, $this->response->getStatus(),
            "Expected status {$code}, got {$this->response->getStatus()}.");
        return $this;
    }

    public function assertOk(): static              { return $this->assertStatus(200); }
    public function assertCreated(): static          { return $this->assertStatus(201); }
    public function assertNoContent(): static        { return $this->assertStatus(204); }
    public function assertNotFound(): static         { return $this->assertStatus(404); }
    public function assertForbidden(): static        { return $this->assertStatus(403); }
    public function assertUnauthorized(): static     { return $this->assertStatus(401); }

    public function assertJson(array $expected): static
    {
        $actual = json_decode($this->response->getBody(), true);
        \PHPUnit\Framework\Assert::assertArrayContains($expected, $actual);
        return $this;
    }

    public function assertSee(string $text): static
    {
        \PHPUnit\Framework\Assert::assertStringContainsString($text, $this->response->getBody());
        return $this;
    }

    public function assertRedirect(?string $uri = null): static
    {
        $status = $this->response->getStatus();
        \PHPUnit\Framework\Assert::assertTrue(
            $status >= 300 && $status < 400,
            "Expected redirect, got status {$status}."
        );
        if ($uri !== null) {
            \PHPUnit\Framework\Assert::assertEquals($uri, $this->response->getHeader('location')[0] ?? '');
        }
        return $this;
    }
}