Skip to content

๐Ÿงช Testing Strategies โ€‹

Comprehensive testing standards for Laravel applications. This section covers PHPUnit setup, testing strategies, mocking techniques, and quality assurance practices.

โš™๏ธ PHPUnit Setup โ€‹

Configure PHPUnit for optimal testing experience.

โœ… Good Example โ€‹

xml
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory suffix="Test.php">./tests/Integration</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
        <exclude>
            <directory>./app/Console</directory>
            <file>./app/Http/Kernel.php</file>
        </exclude>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

โŒ Bad Example โ€‹

xml
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <!-- No test suites defined -->
    <!-- No coverage configuration -->
    <!-- No environment variables -->
    <!-- Uses production database -->
</phpunit>

๐Ÿงช Unit Testing โ€‹

Test individual classes and methods in isolation.

โœ… Good Example โ€‹

php
<?php

namespace Tests\Unit;

use App\Models\User;
use App\Services\UserService;
use App\Repositories\UserRepositoryInterface;
use Tests\TestCase;
use Mockery;

class UserServiceTest extends TestCase
{
    private UserService $userService;
    private UserRepositoryInterface $userRepository;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->userRepository = Mockery::mock(UserRepositoryInterface::class);
        $this->userService = new UserService($this->userRepository);
    }

    public function test_create_user_successfully(): void
    {
        // Arrange
        $userData = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'password123',
        ];
        
        $expectedUser = new User($userData);
        
        $this->userRepository
            ->shouldReceive('create')
            ->once()
            ->with(Mockery::on(function ($data) {
                return isset($data['name']) && 
                       isset($data['email']) && 
                       isset($data['password']) &&
                       $data['password'] !== 'password123'; // Should be hashed
            }))
            ->andReturn($expectedUser);

        // Act
        $result = $this->userService->createUser($userData);

        // Assert
        $this->assertInstanceOf(User::class, $result);
        $this->assertEquals('John Doe', $result->name);
        $this->assertEquals('[email protected]', $result->email);
    }

    public function test_create_user_with_invalid_data_throws_exception(): void
    {
        // Arrange
        $invalidData = [
            'name' => '',
            'email' => 'invalid-email',
        ];

        // Act & Assert
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Name and email are required');
        
        $this->userService->createUser($invalidData);
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }
}

โŒ Bad Example โ€‹

php
<?php

namespace Tests\Unit;

use App\Services\UserService;
use Tests\TestCase;

class UserServiceTest extends TestCase
{
    public function test_create_user()
    {
        // No setup
        // No mocking
        // Uses real database
        // No proper assertions
        
        $userService = new UserService();
        $user = $userService->createUser([
            'name' => 'John',
            'email' => '[email protected]',
        ]);
        
        $this->assertTrue(true); // Meaningless assertion
    }
}

๐Ÿ”ง Feature Testing โ€‹

Test complete user workflows and API endpoints.

โœ… Good Example โ€‹

php
<?php

namespace Tests\Feature;

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

class UserManagementTest extends TestCase
{
    use RefreshDatabase, WithFaker;

    public function test_user_can_create_account(): void
    {
        // Arrange
        $userData = [
            'name' => $this->faker->name(),
            'email' => $this->faker->safeEmail(),
            'password' => 'SecurePassword123!',
            'password_confirmation' => 'SecurePassword123!',
        ];

        // Act
        $response = $this->postJson('/api/users', $userData);

        // Assert
        $response->assertStatus(201)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                ],
            ])
            ->assertJsonMissing(['password']);

        $this->assertDatabaseHas('users', [
            'name' => $userData['name'],
            'email' => $userData['email'],
        ]);
    }

    public function test_user_cannot_create_account_with_invalid_data(): void
    {
        // Arrange
        $invalidData = [
            'name' => '',
            'email' => 'invalid-email',
            'password' => '123',
        ];

        // Act
        $response = $this->postJson('/api/users', $invalidData);

        // Assert
        $response->assertStatus(422)
            ->assertJsonValidationErrors(['name', 'email', 'password']);
    }

    public function test_authenticated_user_can_view_profile(): void
    {
        // Arrange
        $user = User::factory()->create();
        
        // Act
        $response = $this->actingAs($user)
            ->getJson('/api/profile');

        // Assert
        $response->assertStatus(200)
            ->assertJson([
                'data' => [
                    'id' => $user->id,
                    'name' => $user->name,
                    'email' => $user->email,
                ],
            ]);
    }

    public function test_unauthenticated_user_cannot_view_profile(): void
    {
        // Act
        $response = $this->getJson('/api/profile');

        // Assert
        $response->assertStatus(401);
    }
}

โŒ Bad Example โ€‹

php
<?php

namespace Tests\Feature;

use Tests\TestCase;

class UserManagementTest extends TestCase
{
    public function test_user_creation()
    {
        // No database refresh
        // No proper data setup
        // No authentication testing
        // No proper assertions
        
        $response = $this->post('/api/users', [
            'name' => 'John',
            'email' => '[email protected]',
        ]);
        
        $this->assertEquals(200, $response->status());
    }
}

๐ŸŽญ Mocking and Stubbing โ€‹

โœ… Good Example โ€‹

php
<?php

namespace Tests\Unit;

use App\Services\EmailService;
use App\Services\UserService;
use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
use Mockery;

class UserServiceWithEmailTest extends TestCase
{
    public function test_create_user_sends_welcome_email(): void
    {
        // Arrange
        Mail::fake();
        
        $userRepository = Mockery::mock(UserRepositoryInterface::class);
        $emailService = Mockery::mock(EmailService::class);
        
        $userData = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'password123',
        ];
        
        $user = new User($userData);
        
        $userRepository
            ->shouldReceive('create')
            ->once()
            ->andReturn($user);
            
        $emailService
            ->shouldReceive('sendWelcomeEmail')
            ->once()
            ->with($user);

        // Act
        $userService = new UserService($userRepository, $emailService);
        $result = $userService->createUser($userData);

        // Assert
        $this->assertInstanceOf(User::class, $result);
        Mail::assertSent(WelcomeEmail::class);
    }
}

โŒ Bad Example โ€‹

php
<?php

// No mocking - tests depend on external services
class UserServiceTest extends TestCase
{
    public function test_create_user()
    {
        // Uses real email service
        // Sends real emails during testing
        // Tests are slow and unreliable
        
        $userService = new UserService();
        $user = $userService->createUser([
            'name' => 'John',
            'email' => '[email protected]',
        ]);
        
        // How do we verify email was sent?
        $this->assertTrue(true);
    }
}

๐Ÿญ Model Factories โ€‹

โœ… Good Example โ€‹

php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    protected $model = User::class;

    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
            'role' => 'user',
            'is_active' => true,
        ];
    }

    public function admin(): static
    {
        return $this->state(fn (array $attributes) => [
            'role' => 'admin',
        ]);
    }

    public function inactive(): static
    {
        return $this->state(fn (array $attributes) => [
            'is_active' => false,
        ]);
    }

    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

โŒ Bad Example โ€‹

php
<?php

// Hard-coded factory data
class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => 'John Doe', // Always the same
            'email' => '[email protected]', // Always the same
            'password' => 'password', // Plain text password
            'role' => 'user', // No variations
        ];
    }
}

๐Ÿ“Š Test Coverage โ€‹

โœ… Good Example โ€‹

bash
# Run tests with coverage
php artisan test --coverage

# Generate HTML coverage report
php artisan test --coverage-html coverage/

# Minimum coverage threshold
php artisan test --coverage --min=80

โŒ Bad Example โ€‹

bash
# No coverage reporting
php artisan test

# No minimum coverage requirements
# No coverage analysis

๐ŸŽฏ Testing Best Practices โ€‹

โœ… Do's โ€‹

  • Write Tests First - Follow TDD/BDD practices
  • Use Descriptive Names - Test names should explain what they test
  • Arrange-Act-Assert - Structure tests clearly
  • Mock External Dependencies - Keep tests isolated
  • Use Factories - Generate test data consistently
  • Test Edge Cases - Cover error conditions
  • Maintain High Coverage - Aim for 80%+ coverage
  • Use Database Transactions - Keep tests fast
  • Test Both Success and Failure - Cover all scenarios
  • Keep Tests Independent - Each test should be standalone

โŒ Don'ts โ€‹

  • Don't test implementation details - Test behavior, not code
  • Don't skip error cases - Test failure scenarios
  • Don't use real external services - Mock everything
  • Don't write slow tests - Keep tests fast
  • Don't ignore test maintenance - Update tests with code changes
  • Don't test private methods - Test public interfaces
  • Don't skip integration tests - Test complete workflows
  • Don't ignore test data cleanup - Use RefreshDatabase

๐Ÿ”ง Testing Tools โ€‹

Static Analysis โ€‹

bash
# PHPStan
./vendor/bin/phpstan analyse

# Larastan (Laravel-specific)
./vendor/bin/phpstan analyse

# PHP CS Fixer
./vendor/bin/php-cs-fixer fix

# Laravel Pint
./vendor/bin/pint

Code Quality โ€‹

bash
# PHPUnit with coverage
./vendor/bin/phpunit --coverage-text

# Infection (mutation testing)
./vendor/bin/infection

# PHP Mess Detector
./vendor/bin/phpmd app text cleancode,codesize,design,naming,unusedcode

๐Ÿงช Test Quality: Good tests are fast, reliable, maintainable, and provide confidence in your code. Invest time in writing quality tests.

Built with VitePress