0

Feature test / end-to-end test — testing a full user-facing flow

Beginner5 min read·eng-17-003
interview

Concept

Feature test / End-to-end test — a test that verifies a complete user-facing flow, from the HTTP request to the database to the response. Tests the entire stack as an end user would experience it.

Feature test: In Laravel's terminology, a test that makes an HTTP request and asserts the response. Tests a "feature" — a user-facing piece of functionality. Not always end-to-end (might mock external services).

End-to-end (E2E) test: Tests the complete path from the user interface to the database and back, sometimes including real browsers (Cypress, Selenium, Playwright).

What feature tests cover:

  • Full HTTP lifecycle: Route → Middleware → Controller → Service → DB → Response.
  • Authentication and authorization in the real stack.
  • Validation (FormRequest in the real request cycle).
  • Response format and status codes.
  • Database state after the request.

What feature tests catch that unit/integration tests don't:

  • Route registration is correct.
  • Middleware is applied correctly.
  • FormRequest rules work as expected.
  • Auth middleware blocks unauthenticated requests.
  • Response structure matches what the frontend expects.

In Laravel:

  • $this->get('/users'), $this->post('/api/orders', $data) — makes a fake HTTP request through the full stack.
  • $this->actingAs($user) — authenticates a user for the test.
  • assertStatus(200), assertJson(...), assertDatabaseHas(...).

Performance: Slower than unit tests (boots app, DB operations) but much faster than real browser E2E tests.

Code Example

php
<?php
namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class OrderApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_cannot_create_order(): void
    {
        $response = $this->postJson('/api/orders', ['items' => [], 'total' => 100]);
        $response->assertStatus(401); // Unauthenticated
    }

    public function test_user_can_create_order(): void
    {
        $user    = User::factory()->create();
        $product = Product::factory()->create(['price' => 25.00, 'stock' => 10]);

        $response = $this->actingAs($user, 'sanctum') // authenticate as user
            ->postJson('/api/orders', [
                'items' => [
                    ['product_id' => $product->id, 'quantity' => 2],
                ],
            ]);

        $response->assertStatus(201)
                 ->assertJsonStructure([
                     'id', 'status', 'total', 'items' => [['product_id', 'quantity', 'price']],
                 ])
                 ->assertJson(['status' => 'pending', 'total' => 50.00]);

        // Database state
        $this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 50.00]);
        $this->assertDatabaseHas('order_items', ['product_id' => $product->id, 'quantity' => 2]);
        // Stock was decremented
        $this->assertEquals(8, $product->fresh()->stock);
    }

    public function test_validation_fails_without_items(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user, 'sanctum')
            ->postJson('/api/orders', []); // missing items

        $response->assertStatus(422)
                 ->assertJsonValidationErrors(['items']);
    }

    public function test_user_can_only_see_own_orders(): void
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        Order::factory()->for($user1)->create();
        Order::factory()->for($user2)->create();

        $response = $this->actingAs($user1, 'sanctum')->getJson('/api/orders');

        $response->assertStatus(200)
                 ->assertJsonCount(1, 'data'); // only user1's order
    }

    public function test_email_sent_after_order_placed(): void
    {
        \Mail::fake();
        $user    = User::factory()->create();
        $product = Product::factory()->create(['stock' => 5]);

        $this->actingAs($user, 'sanctum')
             ->postJson('/api/orders', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);

        \Mail::assertSent(\App\Mail\OrderConfirmation::class, fn($m) => $m->hasTo($user->email));
    }
}