Laravel предоставляет удобный API для выполнения HTTP-запросов к приложению и проверки ответов в тестах.
Базовые HTTP-запросы
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
public function test_homepage_returns_success(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
public function test_api_returns_users(): void
{
User::factory(3)->create();
$response = $this->getJson('/api/users');
$response->assertOk() // assertStatus(200)
->assertJsonCount(3, 'data');
}
}
Все HTTP-методы
<?php
// GET request
$response = $this->get('/users');
$response = $this->getJson('/api/users'); // Sets Accept: application/json
// POST request
$response = $this->post('/users', [
'name' => 'John',
'email' => '[email protected]',
]);
$response = $this->postJson('/api/users', [
'name' => 'John',
'email' => '[email protected]',
]);
// PUT request
$response = $this->put('/users/1', [
'name' => 'Updated Name',
]);
$response = $this->putJson('/api/users/1', [
'name' => 'Updated Name',
]);
// PATCH request
$response = $this->patch('/users/1', [
'name' => 'Updated Name',
]);
$response = $this->patchJson('/api/users/1', [
'name' => 'Updated Name',
]);
// DELETE request
$response = $this->delete('/users/1');
$response = $this->deleteJson('/api/users/1');
// OPTIONS request
$response = $this->options('/api/users');
// HEAD request
$response = $this->head('/api/users');
Подвох на экзамене: Методы getJson(), postJson(), putJson() и т.д. автоматически устанавливают заголовки Accept: application/json и Content-Type: application/json. Обычные get(), post() используют HTML-запрос. Для API тестов ВСЕГДА используйте *Json() версии.
Заголовки запроса
<?php
$response = $this->withHeaders([
'X-Custom-Header' => 'value',
'Accept-Language' => 'ru',
])->getJson('/api/users');
// Set single header
$response = $this->withHeader('X-Custom', 'value')
->getJson('/api/users');
// Set token
$response = $this->withToken('my-api-token')
->getJson('/api/users');
// Bearer token (same as withToken)
$response = $this->withToken('my-token', 'Bearer')
->getJson('/api/users');
Cookies
<?php
$response = $this->withCookies([
'theme' => 'dark',
'locale' => 'ru',
])->get('/dashboard');
$response = $this->withCookie('session_id', 'abc123')
->get('/dashboard');
// Unencrypted cookies
$response = $this->withUnencryptedCookies([
'name' => 'value',
])->get('/dashboard');
Assertions на ответ
Status Assertions
<?php
$response->assertStatus(200);
$response->assertOk(); // 200
$response->assertCreated(); // 201
$response->assertAccepted(); // 202
$response->assertNoContent(); // 204
$response->assertMovedPermanently(); // 301
$response->assertFound(); // 302
$response->assertNotModified(); // 304
$response->assertBadRequest(); // 400
$response->assertUnauthorized(); // 401
$response->assertPaymentRequired(); // 402
$response->assertForbidden(); // 403
$response->assertNotFound(); // 404
$response->assertMethodNotAllowed(); // 405
$response->assertConflict(); // 409
$response->assertGone(); // 410
$response->assertUnprocessable(); // 422
$response->assertTooManyRequests(); // 429
$response->assertServerError(); // 500
$response->assertServiceUnavailable(); // 503
JSON Assertions
<?php
$response = $this->getJson('/api/users/1');
// Assert exact JSON structure
$response->assertJson([
'data' => [
'id' => 1,
'name' => 'John',
],
]);
// Assert JSON contains (subset matching)
$response->assertJson(fn (\Illuminate\Testing\Fluent\AssertableJson $json) =>
$json->where('data.id', 1)
->where('data.name', 'John')
->missing('data.password')
->etc() // Allow additional properties
);
// Assert exact JSON match (strict)
$response->assertExactJson([
'data' => [
'id' => 1,
'name' => 'John',
'email' => '[email protected]',
],
]);
// Assert JSON path
$response->assertJsonPath('data.name', 'John');
$response->assertJsonPath('data.tags', fn ($tags) => count($tags) > 0);
// Assert JSON structure (keys exist, values can be anything)
$response->assertJsonStructure([
'data' => [
'id',
'name',
'email',
'created_at',
],
]);
// Assert JSON count
$response->assertJsonCount(3, 'data'); // 3 items in data array
// Assert JSON fragment (subset anywhere in response)
$response->assertJsonFragment([
'name' => 'John',
]);
// Assert JSON missing fragment
$response->assertJsonMissingExact([
'name' => 'Jane',
]);
// Assert JSON is array
$response->assertJsonIsArray('data');
// Assert JSON is object
$response->assertJsonIsObject('data');
Fluent JSON Testing (AssertableJson)
<?php
$response->assertJson(fn (\Illuminate\Testing\Fluent\AssertableJson $json) =>
$json->has('data')
->has('data.id')
->has('data.name')
->has('meta.total')
->missing('data.password')
->where('data.id', 1)
->where('data.name', fn ($name) => str_starts_with($name, 'J'))
->whereType('data.id', 'integer')
->whereType('data.name', 'string')
->whereNot('data.role', 'admin')
->etc()
);
// Testing collections
$response->assertJson(fn (\Illuminate\Testing\Fluent\AssertableJson $json) =>
$json->has('data', 3) // 3 items in data
->has('data.0', fn ($json) => // First item
$json->where('id', 1)
->where('name', 'John')
->etc()
)
);
// First item shorthand
$response->assertJson(fn (\Illuminate\Testing\Fluent\AssertableJson $json) =>
$json->has('data', 3, fn ($json) => // Assert count AND first item
$json->where('name', 'John')
->etc()
)
);
Подвох на экзамене: assertJson() выполняет ПОДМНОЖЕСТВО-совпадение (ответ может содержать дополнительные ключи). assertExactJson() требует ТОЧНОГО совпадения. При использовании fluent API, etc() нужен, если вы не проверяете все ключи.
View Assertions
<?php
$response = $this->get('/welcome');
// Assert response contains text
$response->assertSee('Welcome');
$response->assertSeeText('Welcome'); // Only visible text (no HTML tags)
$response->assertDontSee('Error');
// Assert with HTML escaping disabled
$response->assertSee('<strong>Bold</strong>', false); // false = don't escape
// Assert view name
$response->assertViewIs('welcome');
// Assert view has data
$response->assertViewHas('users');
$response->assertViewHas('users', function ($users) {
return $users->count() === 3;
});
$response->assertViewHas('title', 'Welcome Page');
$response->assertViewHasAll([
'users',
'title' => 'Welcome Page',
]);
$response->assertViewMissing('admin_panel');
Redirect Assertions
<?php
$response = $this->post('/login', [
'email' => '[email protected]',
'password' => 'wrong',
]);
$response->assertRedirect('/login'); // Assert redirect to URL
$response->assertRedirectToRoute('login'); // Assert redirect to named route
$response->assertRedirectToSignedRoute('verify-email');
// Follow redirect
$response = $this->followingRedirects()
->post('/login', [
'email' => '[email protected]',
'password' => 'correct',
]);
$response->assertSee('Dashboard');
Header Assertions
<?php
$response->assertHeader('Content-Type', 'application/json');
$response->assertHeaderMissing('X-Custom-Header');
// Download assertions
$response->assertDownload(); // Assert response is download
$response->assertDownload('report.pdf'); // Assert specific filename
Session Assertions
<?php
$response->assertSessionHas('message');
$response->assertSessionHas('message', 'Welcome!');
$response->assertSessionHas('user', function ($value) {
return $value->id === 1;
});
$response->assertSessionHasAll([
'message' => 'Welcome!',
'status' => 'success',
]);
$response->assertSessionMissing('error');
$response->assertSessionHasErrors(['email']);
$response->assertSessionHasErrors([
'email' => 'The email field is required.',
]);
$response->assertSessionHasNoErrors();
$response->assertSessionHasInput('name', 'John');
Аутентификация в тестах
<?php
use App\Models\User;
// Act as a specific user
$user = User::factory()->create();
$response = $this->actingAs($user)
->getJson('/api/profile');
$response->assertOk();
// Act as user on specific guard
$admin = User::factory()->admin()->create();
$response = $this->actingAs($admin, 'admin')
->getJson('/admin/dashboard');
// Test unauthenticated access
$response = $this->getJson('/api/profile');
$response->assertUnauthorized(); // 401
// Sanctum authentication
$user = User::factory()->create();
$token = $user->createToken('test-token');
$response = $this->withToken($token->plainTextToken)
->getJson('/api/profile');
// Or simply:
$response = $this->actingAs($user, 'sanctum')
->getJson('/api/profile');
Подвох на экзамене: actingAs($user) использует guard по умолчанию (web). Для API с Sanctum используйте actingAs($user, 'sanctum'). Забытый guard -- частая ошибка.
Тестирование загрузки файлов
<?php
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
public function test_avatar_upload(): void
{
Storage::fake('public'); // Fake storage disk
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/profile/avatar', [
'avatar' => UploadedFile::fake()->image('avatar.jpg', 200, 200),
]);
$response->assertOk();
// Assert file was stored
Storage::disk('public')->assertExists('avatars/' . $user->id . '.jpg');
// Assert file was NOT stored
Storage::disk('public')->assertMissing('avatars/old-avatar.jpg');
}
public function test_document_upload(): void
{
Storage::fake('local');
$response = $this->actingAs(User::factory()->create())
->postJson('/api/documents', [
'file' => UploadedFile::fake()->create('report.pdf', 1024), // 1MB
]);
$response->assertCreated();
Storage::disk('local')->assertExists('documents/report.pdf');
}
public function test_rejects_large_files(): void
{
Storage::fake('public');
$response = $this->actingAs(User::factory()->create())
->postJson('/api/documents', [
'file' => UploadedFile::fake()->create('huge.pdf', 20000), // 20MB
]);
$response->assertUnprocessable() // 422 validation error
->assertJsonValidationErrors('file');
}
Fake файлы
<?php
use Illuminate\Http\UploadedFile;
// Fake image with dimensions
$file = UploadedFile::fake()->image('photo.jpg', 640, 480);
// Fake image with specific size
$file = UploadedFile::fake()->image('photo.png')->size(500); // 500KB
// Fake file with specific mime type
$file = UploadedFile::fake()->create('document.pdf', 1024, 'application/pdf');
// Fake file with specific content
$file = UploadedFile::fake()->createWithContent(
'data.csv',
"name,email\nJohn,[email protected]"
);
Тестирование Validation
<?php
public function test_user_creation_validation(): void
{
$response = $this->postJson('/api/users', []);
$response->assertUnprocessable()
->assertJsonValidationErrors(['name', 'email', 'password']);
}
public function test_email_must_be_valid(): void
{
$response = $this->postJson('/api/users', [
'name' => 'John',
'email' => 'not-an-email',
'password' => 'password123',
]);
$response->assertJsonValidationErrorFor('email');
}
public function test_email_must_be_unique(): void
{
User::factory()->create(['email' => '[email protected]']);
$response = $this->postJson('/api/users', [
'name' => 'John',
'email' => '[email protected]',
'password' => 'password123',
]);
$response->assertJsonValidationErrors([
'email' => 'The email has already been taken.',
]);
}
// Assert NO validation errors
public function test_valid_data_passes(): void
{
$response = $this->postJson('/api/users', [
'name' => 'John',
'email' => '[email protected]',
'password' => 'SecurePass123!',
]);
$response->assertJsonMissingValidationErrors(['name', 'email']);
}
Тестирование API CRUD (Pest)
<?php
use App\Models\User;
use App\Models\Post;
beforeEach(function () {
$this->user = User::factory()->create();
});
test('can list all posts', function () {
Post::factory(5)->create();
$this->actingAs($this->user, 'sanctum')
->getJson('/api/posts')
->assertOk()
->assertJsonCount(5, 'data')
->assertJsonStructure([
'data' => [
'*' => ['id', 'title', 'body', 'created_at'],
],
'meta' => ['total', 'per_page'],
]);
});
test('can create a post', function () {
$data = [
'title' => 'My First Post',
'body' => 'This is the body of my first post.',
];
$this->actingAs($this->user, 'sanctum')
->postJson('/api/posts', $data)
->assertCreated()
->assertJsonPath('data.title', 'My First Post');
$this->assertDatabaseHas('posts', [
'title' => 'My First Post',
'user_id' => $this->user->id,
]);
});
test('can show a single post', function () {
$post = Post::factory()->create();
$this->actingAs($this->user, 'sanctum')
->getJson("/api/posts/{$post->id}")
->assertOk()
->assertJsonPath('data.id', $post->id);
});
test('can update a post', function () {
$post = Post::factory()->for($this->user)->create();
$this->actingAs($this->user, 'sanctum')
->putJson("/api/posts/{$post->id}", [
'title' => 'Updated Title',
])
->assertOk()
->assertJsonPath('data.title', 'Updated Title');
});
test('can delete a post', function () {
$post = Post::factory()->for($this->user)->create();
$this->actingAs($this->user, 'sanctum')
->deleteJson("/api/posts/{$post->id}")
->assertNoContent();
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
});
test('cannot update someone else\'s post', function () {
$otherUser = User::factory()->create();
$post = Post::factory()->for($otherUser)->create();
$this->actingAs($this->user, 'sanctum')
->putJson("/api/posts/{$post->id}", ['title' => 'Hacked'])
->assertForbidden();
});
Тестирование с middleware
<?php
// Disable all middleware
$response = $this->withoutMiddleware()
->getJson('/api/users');
// Disable specific middleware
$response = $this->withoutMiddleware(ThrottleRequests::class)
->getJson('/api/users');
// Keep middleware but test exceptions
$response = $this->withMiddleware()
->getJson('/api/admin')
->assertForbidden();
Подвох на экзамене: withoutMiddleware() без аргументов отключает ВСЕ middleware, включая авторизацию, CSRF и т.д. Для точного тестирования лучше отключать конкретное middleware.