Browser Testing
PhpSpec includes a lightweight HTTP client for testing web endpoints. Use visit(), get(), post(), put(), patch(), or delete() to make requests and assert on the response.
Configuration
Add base_url to your phpspec.json:
{
"base_url": "http://localhost:8080"
}
Quick Example
<?php describe('User API', function () { it('returns a user by id', function () { $response = get('/api/users/1'); expect($response)->toBeOk(); expect($response)->toHaveStatus(200); expect($response->json['name'])->toBe('Chuck Norris'); expect($response)->toHavePath('data.user.name', 'Chuck Norris'); }); it('creates a user', function () { $response = post('/api/users', json: ['name' => 'Chuck']); expect($response)->toHaveStatus(201); expect($response)->toHavePath('name', 'Chuck'); }); it('updates a user', function () { $response = put('/api/users/1', json: ['name' => 'Updated']); expect($response)->toBeOk(); }); it('patches a user', function () { $response = patch('/api/users/1', json: ['name' => 'Patched']); expect($response)->toBeOk(); }); it('deletes a user', function () { $response = delete('/api/users/1'); expect($response)->toHaveStatus(204); }); });
Making Requests
| Function | Description |
|---|---|
visit(string $path) | GET request (alias for get()) |
get(string $path) | GET request |
post(string $path, array $options = []) | POST request |
put(string $path, array $options = []) | PUT request |
patch(string $path, array $options = []) | PATCH request |
delete(string $path, array $options = []) | DELETE request |
A redirect is not followed: the response is the one the server sent, its 3xx status and Location header included, so toRedirectTo() can check it. Visit the Location to check the page it leads to.
Request Options
The $options array supports:
| Key | Type | Description |
|---|---|---|
json | array | JSON-encoded as the request body; sets Content-Type: application/json |
body | string | Raw request body |
headers | array | Additional HTTP headers (name => value) |
callback | callable | Told each exchange: ($method, $url, $status, $body, $headers). Overrides the default one, which attaches the request and response to the report |
Response Object
All functions return a Response with the following properties:
| Property | Type | Description |
|---|---|---|
$response->status | int | HTTP status code |
$response->body | string | Raw response body |
$response->json | array | Body decoded as JSON (lazy, json_decode(..., true)) |
$response->headers | array | Response headers (name => value), named as the server sent them; toHaveHeader() finds a name in any case |
Response Matchers
toBeOk()
Asserts the response status is 200.
expect($response)->toBeOk();
toBeBad()
Asserts the response status is 400.
expect($response)->toBeBad();
toHaveStatus(int $code)
Asserts the response status matches the given code.
expect($response)->toHaveStatus(201); expect($response)->toHaveStatus(404);
toHavePath(string $path, mixed $expected)
Asserts a value at a dot-notation path in the JSON body.
expect($response)->toHavePath('data.user.name', 'Chuck Norris'); expect($response)->toHavePath('meta.total', 42);
toHaveHeader(string $name, ?string $value = null)
Asserts the response has a header, its name in any case, as HTTP names are. Optionally checks its value.
expect($response)->toHaveHeader('Content-Type'); expect($response)->toHaveHeader('Content-Type', 'application/json'); expect($response)->not()->toHaveHeader('X-Debug');
toRedirectTo(string $url)
Asserts the response is a 3xx redirect with a matching Location header.
expect($response)->toRedirectTo('/dashboard'); expect($response)->toRedirectTo('https://example.com/login');
Existing Matchers That Work on Response Properties
You don't need special matchers for everything. Use the ones you already know:
// Body expect($response->body)->toContain('hello'); expect($response->body)->toMatch('/id":\s*\d+/'); // JSON expect($response->json)->toHaveKey('name'); expect($response->json)->toContain('Chuck Norris'); expect($response->json['items'])->toHaveCount(3); // Headers, by name in any case expect($response)->toHaveHeader('Content-Type', 'application/json'); // Direct value access expect($response->json['name'])->toBe('Chuck Norris'); expect($response->status)->toBeGreaterThan(199);
PSR-7 Support
The response matchers (toBeOk, toBeBad, toHaveStatus, toHavePath) also work with any PSR-7 ResponseInterface. No hard dependency is required: install psr/http-message and the matchers detect it automatically:
$ composer require psr/http-message
This means Symfony and Laravel responses work out of the box via their PSR-7 bridges:
// Symfony: use symfony/psr-http-message-bridge $psr7Response = $psrFactory->createResponse($symfonyResponse); expect($psr7Response)->toBeOk(); expect($psr7Response)->toHavePath('data.name', 'Chuck'); // Laravel: use nyholm/psr7 $psr7Response = app('Psr\Http\Message\ResponseInterface'); expect($psr7Response)->toHaveStatus(201);
Replacing the Browser
The bundled client is the default implementation of one contract:
namespace PhpSpec\Browser; interface Browser { public function request(string $method, string $url, array $options = []): Response; }
An extension can put anything behind visit() (BrowserKit, a headless
browser) by implementing it and mapping what it drives onto the canonical
Response, which is what keeps every response matcher working unchanged:
extensions: browser: Acme\BrowserKitBrowser
The key is singular because exactly one browser drives the DSL. A package can
also auto-register via extra.phpspec (see Extensions); the
config's choice wins, and two auto-discovered browsers with no config choice
are refused by name.