1. php
  2. /testing
  3. /integration-testing

Integration Testing in PHP

Introduction to Integration Testing

Integration testing focuses on verifying that different components, modules, or services work correctly when combined. Unlike unit tests that test isolated components, integration tests validate the interactions between multiple parts of your application.

Types of Integration Testing

Database Integration Testing

<?php
use PHPUnit\Framework\TestCase;

class UserRepositoryIntegrationTest extends TestCase
{
    private $pdo;
    private $userRepository;
    
    protected function setUp(): void
    {
        // Use test database
        $this->pdo = new PDO('sqlite::memory:');
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        // Create test schema
        $this->createTestSchema();
        
        $this->userRepository = new UserRepository($this->pdo);
    }
    
    private function createTestSchema(): void
    {
        $this->pdo->exec('
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email VARCHAR(255) UNIQUE NOT NULL,
                name VARCHAR(255) NOT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ');
    }
    
    public function testCreateAndRetrieveUser(): void
    {
        // Create user
        $userData = [
            'name' => 'John Doe',
            'email' => '[email protected]'
        ];
        
        $userId = $this->userRepository->create($userData);
        
        // Retrieve and verify
        $user = $this->userRepository->findById($userId);
        
        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user['name']);
        $this->assertEquals('[email protected]', $user['email']);
    }
    
    public function testUserEmailUniqueness(): void
    {
        // Create first user
        $this->userRepository->create([
            'name' => 'John Doe',
            'email' => '[email protected]'
        ]);
        
        // Attempt to create user with same email
        $this->expectException(PDOException::class);
        
        $this->userRepository->create([
            'name' => 'Jane Doe',
            'email' => '[email protected]'
        ]);
    }
    
    public function testTransactionalBehavior(): void
    {
        $this->pdo->beginTransaction();
        
        try {
            $this->userRepository->create([
                'name' => 'Test User',
                'email' => '[email protected]'
            ]);
            
            // Simulate error
            throw new Exception('Test error');
            
        } catch (Exception $e) {
            $this->pdo->rollBack();
        }
        
        // Verify user was not created
        $users = $this->userRepository->findAll();
        $this->assertCount(0, $users);
    }
}
?>

API Integration Testing

<?php
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;

class PaymentServiceIntegrationTest extends TestCase
{
    private $client;
    private $paymentService;
    
    protected function setUp(): void
    {
        $this->client = new Client([
            'base_uri' => 'https://api.example.com/v1/',
            'timeout' => 30
        ]);
        
        $this->paymentService = new PaymentService($this->client);
    }
    
    public function testProcessPaymentSuccess(): void
    {
        // Mock successful payment response
        $mock = new MockHandler([
            new Response(200, [], json_encode([
                'id' => 'txn_123456',
                'status' => 'completed',
                'amount' => 1000
            ]))
        ]);
        
        $handlerStack = HandlerStack::create($mock);
        $client = new Client(['handler' => $handlerStack]);
        $paymentService = new PaymentService($client);
        
        $result = $paymentService->processPayment([
            'amount' => 1000,
            'currency' => 'USD',
            'card_token' => 'tok_123456'
        ]);
        
        $this->assertTrue($result['success']);
        $this->assertEquals('txn_123456', $result['transaction_id']);
    }
    
    public function testProcessPaymentFailure(): void
    {
        // Mock failed payment response
        $mock = new MockHandler([
            new Response(400, [], json_encode([
                'error' => 'insufficient_funds',
                'message' => 'Insufficient funds'
            ]))
        ]);
        
        $handlerStack = HandlerStack::create($mock);
        $client = new Client(['handler' => $handlerStack]);
        $paymentService = new PaymentService($client);
        
        $result = $paymentService->processPayment([
            'amount' => 1000,
            'currency' => 'USD',
            'card_token' => 'tok_invalid'
        ]);
        
        $this->assertFalse($result['success']);
        $this->assertEquals('insufficient_funds', $result['error']);
    }
    
    /**
     * @group integration
     * @group external
     */
    public function testRealApiConnection(): void
    {
        // Skip if not in integration test environment
        if (!getenv('RUN_INTEGRATION_TESTS')) {
            $this->markTestSkipped('Integration tests disabled');
        }
        
        // Test with real API (use test credentials)
        $paymentService = new PaymentService(new Client([
            'base_uri' => getenv('PAYMENT_API_URL'),
            'headers' => [
                'Authorization' => 'Bearer ' . getenv('TEST_API_KEY')
            ]
        ]));
        
        $result = $paymentService->validateApiConnection();
        $this->assertTrue($result);
    }
}
?>

Service Integration Testing

<?php
use PHPUnit\Framework\TestCase;

class OrderProcessingIntegrationTest extends TestCase
{
    private $container;
    private $orderService;
    private $inventoryService;
    private $paymentService;
    private $emailService;
    
    protected function setUp(): void
    {
        // Set up dependency injection container
        $this->container = new Container();
        
        // Configure test services
        $this->setupTestServices();
        
        $this->orderService = $this->container->get(OrderService::class);
        $this->inventoryService = $this->container->get(InventoryService::class);
        $this->paymentService = $this->container->get(PaymentService::class);
        $this->emailService = $this->container->get(EmailService::class);
    }
    
    private function setupTestServices(): void
    {
        // Use test database
        $this->container->set('db', function() {
            $pdo = new PDO('sqlite::memory:');
            $this->createTestSchema($pdo);
            return $pdo;
        });
        
        // Mock external services
        $this->container->set(PaymentGateway::class, function() {
            return new MockPaymentGateway();
        });
        
        $this->container->set(EmailProvider::class, function() {
            return new MockEmailProvider();
        });
    }
    
    public function testCompleteOrderWorkflow(): void
    {
        // Setup test data
        $product = $this->createTestProduct();
        $customer = $this->createTestCustomer();
        
        // Add inventory
        $this->inventoryService->addStock($product['id'], 10);
        
        // Create order
        $orderData = [
            'customer_id' => $customer['id'],
            'items' => [
                [
                    'product_id' => $product['id'],
                    'quantity' => 2,
                    'price' => 29.99
                ]
            ],
            'payment_method' => 'credit_card',
            'payment_token' => 'tok_test_123'
        ];
        
        $order = $this->orderService->processOrder($orderData);
        
        // Verify order was created
        $this->assertNotNull($order['id']);
        $this->assertEquals('completed', $order['status']);
        
        // Verify inventory was updated
        $stock = $this->inventoryService->getStock($product['id']);
        $this->assertEquals(8, $stock); // 10 - 2
        
        // Verify payment was processed
        $payment = $this->paymentService->getPayment($order['payment_id']);
        $this->assertEquals('completed', $payment['status']);
        
        // Verify email was sent
        $emails = $this->emailService->getSentEmails();
        $this->assertCount(1, $emails);
        $this->assertEquals($customer['email'], $emails[0]['to']);
        $this->assertStringContainsString('Order Confirmation', $emails[0]['subject']);
    }
    
    public function testOrderFailsWhenInsufficientStock(): void
    {
        $product = $this->createTestProduct();
        $customer = $this->createTestCustomer();
        
        // Add limited inventory
        $this->inventoryService->addStock($product['id'], 1);
        
        $orderData = [
            'customer_id' => $customer['id'],
            'items' => [
                [
                    'product_id' => $product['id'],
                    'quantity' => 5, // More than available
                    'price' => 29.99
                ]
            ]
        ];
        
        $this->expectException(InsufficientStockException::class);
        $this->orderService->processOrder($orderData);
    }
    
    public function testOrderRollbackOnPaymentFailure(): void
    {
        $product = $this->createTestProduct();
        $customer = $this->createTestCustomer();
        
        $this->inventoryService->addStock($product['id'], 10);
        
        $orderData = [
            'customer_id' => $customer['id'],
            'items' => [
                [
                    'product_id' => $product['id'],
                    'quantity' => 2,
                    'price' => 29.99
                ]
            ],
            'payment_method' => 'credit_card',
            'payment_token' => 'tok_will_fail'
        ];
        
        try {
            $this->orderService->processOrder($orderData);
            $this->fail('Expected PaymentException was not thrown');
        } catch (PaymentException $e) {
            // Verify inventory was rolled back
            $stock = $this->inventoryService->getStock($product['id']);
            $this->assertEquals(10, $stock); // Should be unchanged
            
            // Verify no order was created
            $orders = $this->orderService->getOrdersByCustomer($customer['id']);
            $this->assertCount(0, $orders);
        }
    }
    
    private function createTestProduct(): array
    {
        return [
            'id' => 1,
            'name' => 'Test Product',
            'price' => 29.99,
            'sku' => 'TEST-001'
        ];
    }
    
    private function createTestCustomer(): array
    {
        return [
            'id' => 1,
            'name' => 'John Doe',
            'email' => '[email protected]'
        ];
    }
}
?>

Testing Strategies

Test Data Management

<?php
class DatabaseTestCase extends PHPUnit\Framework\TestCase
{
    protected $pdo;
    protected $fixtures = [];
    
    protected function setUp(): void
    {
        $this->pdo = $this->createTestDatabase();
        $this->loadFixtures();
    }
    
    protected function tearDown(): void
    {
        $this->cleanupDatabase();
    }
    
    private function createTestDatabase(): PDO
    {
        $pdo = new PDO('sqlite::memory:');
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        // Create schema
        $schema = file_get_contents(__DIR__ . '/fixtures/schema.sql');
        $pdo->exec($schema);
        
        return $pdo;
    }
    
    private function loadFixtures(): void
    {
        foreach ($this->fixtures as $table => $data) {
            $this->insertFixtureData($table, $data);
        }
    }
    
    private function insertFixtureData(string $table, array $data): void
    {
        foreach ($data as $row) {
            $columns = implode(', ', array_keys($row));
            $placeholders = ':' . implode(', :', array_keys($row));
            
            $sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($row);
        }
    }
    
    private function cleanupDatabase(): void
    {
        // Clear all tables
        $tables = $this->pdo->query("SELECT name FROM sqlite_master WHERE type='table'")
                           ->fetchAll(PDO::FETCH_COLUMN);
        
        foreach ($tables as $table) {
            $this->pdo->exec("DELETE FROM {$table}");
        }
    }
    
    protected function assertDatabaseHas(string $table, array $criteria): void
    {
        $conditions = [];
        $values = [];
        
        foreach ($criteria as $column => $value) {
            $conditions[] = "{$column} = ?";
            $values[] = $value;
        }
        
        $sql = "SELECT COUNT(*) FROM {$table} WHERE " . implode(' AND ', $conditions);
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($values);
        
        $count = $stmt->fetchColumn();
        
        $this->assertGreaterThan(0, $count, 
            "Failed asserting that table '{$table}' contains matching record");
    }
    
    protected function assertDatabaseCount(string $table, int $expectedCount): void
    {
        $stmt = $this->pdo->query("SELECT COUNT(*) FROM {$table}");
        $actualCount = $stmt->fetchColumn();
        
        $this->assertEquals($expectedCount, $actualCount,
            "Failed asserting that table '{$table}' has {$expectedCount} records");
    }
}

class UserIntegrationTest extends DatabaseTestCase
{
    protected $fixtures = [
        'users' => [
            ['id' => 1, 'name' => 'John Doe', 'email' => '[email protected]'],
            ['id' => 2, 'name' => 'Jane Smith', 'email' => '[email protected]']
        ]
    ];
    
    public function testUserCreation(): void
    {
        $userService = new UserService($this->pdo);
        
        $userData = [
            'name' => 'Bob Johnson',
            'email' => '[email protected]'
        ];
        
        $user = $userService->createUser($userData);
        
        $this->assertDatabaseHas('users', [
            'name' => 'Bob Johnson',
            'email' => '[email protected]'
        ]);
        
        $this->assertDatabaseCount('users', 3); // 2 fixtures + 1 new
    }
}
?>

Environment Configuration

<?php
class TestEnvironment
{
    public static function setup(): void
    {
        // Set test environment variables
        putenv('APP_ENV=testing');
        putenv('DB_CONNECTION=sqlite');
        putenv('DB_DATABASE=:memory:');
        putenv('CACHE_DRIVER=array');
        putenv('QUEUE_CONNECTION=sync');
        putenv('MAIL_MAILER=array');
    }
    
    public static function isIntegrationTestEnabled(): bool
    {
        return getenv('RUN_INTEGRATION_TESTS') === 'true';
    }
    
    public static function skipIfIntegrationDisabled(): void
    {
        if (!self::isIntegrationTestEnabled()) {
            throw new PHPUnit\Framework\SkippedTestError(
                'Integration tests are disabled'
            );
        }
    }
}

abstract class IntegrationTestCase extends PHPUnit\Framework\TestCase
{
    protected function setUp(): void
    {
        TestEnvironment::skipIfIntegrationDisabled();
        TestEnvironment::setup();
        
        parent::setUp();
    }
}
?>

Container Testing

<?php
use Psr\Container\ContainerInterface;

class ContainerIntegrationTest extends PHPUnit\Framework\TestCase
{
    private $container;
    
    protected function setUp(): void
    {
        $this->container = $this->createTestContainer();
    }
    
    private function createTestContainer(): ContainerInterface
    {
        $container = new Container();
        
        // Configure services
        $container->set('config', [
            'database' => [
                'dsn' => 'sqlite::memory:',
                'options' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
            ]
        ]);
        
        $container->set(PDO::class, function($c) {
            $config = $c->get('config')['database'];
            return new PDO($config['dsn'], null, null, $config['options']);
        });
        
        $container->set(UserRepository::class, function($c) {
            return new UserRepository($c->get(PDO::class));
        });
        
        $container->set(UserService::class, function($c) {
            return new UserService($c->get(UserRepository::class));
        });
        
        return $container;
    }
    
    public function testServiceResolution(): void
    {
        $userService = $this->container->get(UserService::class);
        
        $this->assertInstanceOf(UserService::class, $userService);
    }
    
    public function testServiceDependencies(): void
    {
        $userService = $this->container->get(UserService::class);
        $userRepository = $this->container->get(UserRepository::class);
        
        // Verify that the same repository instance is injected
        $reflection = new ReflectionClass($userService);
        $property = $reflection->getProperty('repository');
        $property->setAccessible(true);
        
        $this->assertSame($userRepository, $property->getValue($userService));
    }
    
    public function testSingletonBehavior(): void
    {
        $service1 = $this->container->get(UserService::class);
        $service2 = $this->container->get(UserService::class);
        
        $this->assertSame($service1, $service2);
    }
}
?>

CI/CD Integration

GitHub Actions Configuration

# .github/workflows/integration-tests.yml
name: Integration Tests

on: [push, pull_request]

jobs:
  integration:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
      
      redis:
        image: redis:alpine
        ports:
          - 6379:6379
        options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        extensions: mbstring, dom, fileinfo, mysql, redis
        coverage: xdebug
    
    - name: Install dependencies
      run: composer install --prefer-dist --no-progress
    
    - name: Setup environment
      run: |
        cp .env.testing .env
        php artisan key:generate
    
    - name: Run database migrations
      run: php artisan migrate --force
    
    - name: Run integration tests
      env:
        RUN_INTEGRATION_TESTS: true
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: test_db
        DB_USERNAME: root
        DB_PASSWORD: password
        REDIS_HOST: 127.0.0.1
        REDIS_PORT: 6379
      run: vendor/bin/phpunit --group integration

Test Configuration

<?php
// phpunit.xml configuration for integration tests
?>
<phpunit>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory suffix="Test.php">./tests/Integration</directory>
        </testsuite>
    </testsuites>
    
    <groups>
        <include>
            <group>integration</group>
        </include>
        <exclude>
            <group>external</group>
        </exclude>
    </groups>
    
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="RUN_INTEGRATION_TESTS" value="true"/>
        <env name="DB_CONNECTION" value="mysql"/>
        <env name="DB_DATABASE" value="test_db"/>
    </php>
</phpunit>

Integration testing ensures that your application components work together correctly, providing confidence in your system's overall functionality and helping catch issues that unit tests might miss.