0

Controller authorization — $this->authorize(), can()

Intermediate5 min read·lv-08-007

Concept

Controller testing in Laravel spans from unit tests (testing the controller method in isolation) to feature tests (testing the full HTTP lifecycle). Feature tests are preferred because they test the complete request/response cycle including middleware, routing, and model binding.

HTTP testing methods:

  • $this->get(string $url) / $this->post() / $this->put() / $this->patch() / $this->delete(): Make HTTP requests.
  • $this->getJson() / $this->postJson(): Set Accept: application/json, parse response as JSON.
  • $this->actingAs(User $user, string $guard = 'web'): Authenticate as a user for the test.
  • $this->withHeaders(array $headers): Add custom headers.

Response assertions:

  • ->assertStatus(int $code) / ->assertOk() / ->assertCreated() / ->assertNotFound() / ->assertForbidden() / ->assertUnauthorized().
  • ->assertJson(array $data): Assert response contains the given JSON data.
  • ->assertJsonStructure(array $structure): Assert response has the given JSON structure.
  • ->assertRedirect(string $url): Assert redirect response.
  • ->assertViewIs(string $view): Assert the correct view was rendered.
  • ->assertSessionHas(string $key): Assert a session value was set.

Database assertions:

  • $this->assertDatabaseHas(string $table, array $data): Assert a row exists.
  • $this->assertDatabaseMissing(string $table, array $data): Assert a row doesn't exist.
  • $this->assertDatabaseCount(string $table, int $count): Assert row count.

RefreshDatabase trait: Wraps each test in a transaction that rolls back after the test. Keeps test data isolated.

Code Example

php
<?php
namespace Tests\Feature;

use App\Models\User;
use App\Models\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase, WithFaker;

    public function test_index_returns_users_list(): void
    {
        User::factory()->count(3)->create();
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)->getJson('/api/users');

        $response->assertOk()
            ->assertJsonCount(4, 'data') // 3 + admin
            ->assertJsonStructure(['data' => [['id', 'name', 'email']]]);
    }

    public function test_store_creates_user(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)->postJson('/api/users', [
            'name'                  => 'Alice Smith',
            'email'                 => 'alice@example.com',
            'password'              => 'password123',
            'password_confirmation' => 'password123',
            'role'                  => 'viewer',
        ]);

        $response->assertCreated()
            ->assertJsonPath('data.email', 'alice@example.com');

        $this->assertDatabaseHas('users', ['email' => 'alice@example.com']);
    }

    public function test_unauthenticated_user_cannot_create(): void
    {
        $this->postJson('/api/users', ['name' => 'Bob'])->assertUnauthorized();
    }

    public function test_destroy_deletes_user(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $user  = User::factory()->create();

        $this->actingAs($admin)->deleteJson("/api/users/{$user->id}")
            ->assertNoContent();

        $this->assertDatabaseMissing('users', ['id' => $user->id]);
    }

    public function test_validation_fails_on_invalid_email(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $this->actingAs($admin)->postJson('/api/users', ['email' => 'not-an-email'])
            ->assertUnprocessable()
            ->assertJsonValidationErrors(['email', 'name', 'password']);
    }
}