Integration test — testing multiple real components working together
Concept
Integration test — a test that verifies multiple real components working together. Unlike unit tests, integration tests use the real dependencies (real database, real service container, real cache).
What "integration" means: The INTEGRATION between components — how they work when combined. An integration test for OrderService with a real database verifies that the SQL queries actually work, that the schema is correct, that constraints are honored.
What integration tests catch that unit tests don't:
- SQL queries are actually valid and return the expected data.
- Schema migrations match the queries.
- Cache invalidation actually works.
- Middleware chains behave correctly.
- Service provider bindings are correctly wired.
In Laravel:
extends Tests\TestCase— boots the full Laravel application.use RefreshDatabase— runs migrations before each test, rolls back after.use DatabaseTransactions— wraps each test in a transaction, rolls back after. Faster.
Integration test characteristics:
- Slower than unit tests (DB setup, application boot).
- More realistic than unit tests (real behavior, not mocked).
- Fewer in number (slow to run, use strategically).
vs Feature tests: Integration tests focus on component-to-component behavior. Feature tests focus on full HTTP request/response cycles (user-facing features).
vs Unit tests: Integration tests allow real dependencies. Unit tests mock everything.
Code Example
<?php
// INTEGRATION TEST — tests OrderService with real database
namespace Tests\Integration;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class OrderServiceTest extends TestCase
{
use RefreshDatabase; // migrate + rollback for each test
private OrderService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = app(OrderService::class); // real service with real dependencies
}
public function test_creates_order_in_database(): void
{
$user = User::factory()->create();
$order = $this->service->place($user, [
'items' => [['product_id' => 1, 'quantity' => 2, 'price' => 10.00]],
'total' => 20.00,
]);
// Assert the order exists in the real database
$this->assertDatabaseHas('orders', [
'id' => $order->id,
'user_id' => $user->id,
'total' => 20.00,
'status' => 'pending',
]);
// Assert items exist in the real database
$this->assertDatabaseHas('order_items', [
'order_id' => $order->id,
'product_id' => 1,
'quantity' => 2,
]);
}
public function test_cancels_order_and_restores_stock(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['stock' => 10]);
$order = $this->service->place($user, [
'items' => [['product_id' => $product->id, 'quantity' => 3, 'price' => 5.00]],
'total' => 15.00,
]);
$this->service->cancel($order);
// Verify status changed in DB
$this->assertDatabaseHas('orders', ['id' => $order->id, 'status' => 'cancelled']);
// Verify stock was restored in DB
$this->assertEquals(10, $product->fresh()->stock); // stock back to original
}
public function test_cannot_place_order_with_insufficient_stock(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['stock' => 1]); // only 1 in stock
$this->expectException(\App\Exceptions\InsufficientStockException::class);
$this->service->place($user, [
'items' => [['product_id' => $product->id, 'quantity' => 5]], // want 5, only 1 available
'total' => 50.00,
]);
}
public function test_dispatches_event_after_order_placed(): void
{
\Event::fake([\App\Events\OrderPlaced::class]);
$user = User::factory()->create();
$order = $this->service->place($user, ['items' => [], 'total' => 0]);
\Event::assertDispatched(\App\Events\OrderPlaced::class, function ($e) use ($order) {
return $e->order->id === $order->id;
});
}
}