1. php
  2. /testing
  3. /phpunit

PHPUnit Testing Guide

Introduction to PHPUnit

PHPUnit is the de facto standard for unit testing in PHP, created by Sebastian Bergmann. But to understand why PHPUnit matters, we first need to understand why testing itself is fundamental to professional software development.

Why Testing is Critical in Modern Development

Quality Assurance: Testing helps catch bugs before they reach production, reducing the cost and impact of defects. It's exponentially more expensive to fix bugs in production than during development.

Confidence in Changes: A comprehensive test suite allows developers to refactor code, add features, and fix bugs with confidence. Without tests, even small changes can introduce unexpected side effects.

Documentation: Well-written tests serve as living documentation, showing how code is intended to be used and what behaviors are expected.

Design Improvement: Writing tests often reveals design problems in your code. If something is hard to test, it's often a sign that the design could be improved.

Regression Prevention: Tests prevent old bugs from reappearing when code changes. This is especially valuable in large applications with multiple developers.

Understanding Different Types of Testing

Unit Tests: Test individual components (usually methods or small classes) in isolation. These are fast, focused, and make up the majority of most test suites. PHPUnit excels at unit testing.

Integration Tests: Test how different components work together. These might test database interactions, API calls, or how multiple classes collaborate.

Functional Tests: Test complete features from an end-user perspective. These are slower but provide high confidence that the system works as intended.

End-to-End Tests: Test the entire application flow, often including browser automation. These are the slowest but most comprehensive.

The testing pyramid suggests having many unit tests, fewer integration tests, and even fewer end-to-end tests. PHPUnit is primarily focused on the unit and integration testing layers.

Test-Driven Development (TDD) Philosophy

TDD is a development approach where you write tests before writing the implementation code. The cycle is:

  1. Red: Write a failing test for the next bit of functionality
  2. Green: Write the minimum code necessary to make the test pass
  3. Refactor: Improve the code while keeping tests green

TDD offers several benefits:

  • Forces you to think about the interface before implementation
  • Ensures every line of code has a test
  • Leads to more modular, testable designs
  • Provides immediate feedback on code changes

Behavior-Driven Development (BDD) Concepts

BDD extends TDD by focusing on the behavior and business value rather than just technical implementation. While PHPUnit is primarily a unit testing tool, understanding BDD helps write better, more meaningful tests.

Testing Anti-Patterns to Avoid

Testing Implementation Details: Tests should focus on behavior, not internal implementation. If you refactor code without changing behavior, tests shouldn't break.

Fragile Tests: Tests that break frequently due to minor changes become a maintenance burden rather than a help.

Slow Tests: Test suites that take too long to run discourage developers from running them frequently.

Testing Everything: Not every line of code needs a test. Focus on business logic, edge cases, and areas prone to bugs.

The Role of Mocking and Test Doubles

Real applications have dependencies - databases, APIs, file systems, etc. Testing these directly would be slow and unreliable. Test doubles (mocks, stubs, fakes) replace these dependencies with controlled, predictable alternatives.

Mocks: Verify that certain methods are called with expected parameters Stubs: Return predetermined responses to method calls Fakes: Working implementations with shortcuts (like in-memory databases)

Understanding when and how to use test doubles is crucial for effective unit testing.

PHPUnit in the PHP Ecosystem

PHPUnit integrates seamlessly with modern PHP development tools:

  • Composer: For dependency management and autoloading
  • Continuous Integration: Automated testing on code changes
  • Code Coverage: Measuring how much code is tested
  • IDE Integration: Running tests directly from development environments

Testing Best Practices Overview

Arrange-Act-Assert Pattern: Structure tests with clear setup, execution, and verification phases.

Descriptive Test Names: Test names should clearly describe what behavior is being tested.

Independent Tests: Each test should be able to run independently and in any order.

Single Assertion Focus: Each test should verify one specific behavior.

Test Data Management: Use factories, builders, or fixtures to create test data consistently.

Now let's dive into how PHPUnit helps you implement these testing principles effectively:

Installation and Setup

Installation via Composer

# Install PHPUnit as a development dependency
composer require --dev phpunit/phpunit

# Install specific version
composer require --dev phpunit/phpunit ^10.0

# Verify installation
./vendor/bin/phpunit --version

# Create phpunit.xml configuration file
./vendor/bin/phpunit --generate-configuration

PHPUnit Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         cacheDirectory=".phpunit.cache"
         executionOrder="depends,defects"
         requireCoverageMetadata="true"
         beStrictAboutCoverageMetadata="true"
         beStrictAboutOutputDuringTests="true"
         failOnRisky="true"
         failOnWarning="true">
    
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    
    <source>
        <include>
            <directory>src</directory>
        </include>
        <exclude>
            <directory>src/Views</directory>
            <file>src/bootstrap.php</file>
        </exclude>
    </source>
    
    <logging>
        <junit outputFile="build/logs/junit.xml"/>
        <teamcity outputFile="build/logs/teamcity.txt"/>
    </logging>
    
    <coverage>
        <report>
            <html outputDirectory="build/coverage"/>
            <text outputFile="build/coverage.txt"/>
            <xml outputDirectory="build/coverage/xml"/>
        </report>
    </coverage>
</phpunit>

Writing Your First Test

Basic Test Structure

<?php
// tests/Unit/CalculatorTest.php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAddition(): void
    {
        $calculator = new Calculator();
        $result = $calculator->add(2, 3);
        
        $this->assertEquals(5, $result);
    }
    
    public function testSubtraction(): void
    {
        $calculator = new Calculator();
        $result = $calculator->subtract(10, 4);
        
        $this->assertEquals(6, $result);
    }
    
    public function testDivisionByZero(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Division by zero is not allowed');
        
        $calculator = new Calculator();
        $calculator->divide(10, 0);
    }
}

// src/Calculator.php
class Calculator
{
    public function add(float $a, float $b): float
    {
        return $a + $b;
    }
    
    public function subtract(float $a, float $b): float
    {
        return $a - $b;
    }
    
    public function multiply(float $a, float $b): float
    {
        return $a * $b;
    }
    
    public function divide(float $a, float $b): float
    {
        if ($b === 0.0) {
            throw new InvalidArgumentException('Division by zero is not allowed');
        }
        
        return $a / $b;
    }
}

Test Naming Conventions

<?php
class UserServiceTest extends TestCase
{
    // Method name should describe what is being tested
    public function testCreateUserWithValidDataReturnsUser(): void
    {
        // Test implementation
    }
    
    // Alternative naming using @test annotation
    /**
     * @test
     */
    public function userCreationWithInvalidEmailThrowsException(): void
    {
        // Test implementation
    }
    
    // Data provider tests
    /**
     * @dataProvider validEmailProvider
     */
    public function testEmailValidation(string $email, bool $expected): void
    {
        $validator = new EmailValidator();
        $result = $validator->isValid($email);
        
        $this->assertEquals($expected, $result);
    }
    
    public function validEmailProvider(): array
    {
        return [
            ['[email protected]', true],
            ['[email protected]', true],
            ['invalid-email', false],
            ['missing@', false],
            ['@domain.com', false],
        ];
    }
}
?>

PHPUnit Assertions

Basic Assertions

<?php
class AssertionExamplesTest extends TestCase
{
    public function testBasicAssertions(): void
    {
        // Equality assertions
        $this->assertEquals(5, 2 + 3);
        $this->assertNotEquals(4, 2 + 3);
        $this->assertSame(5, 5); // Strict comparison (===)
        $this->assertNotSame('5', 5); // Different types
        
        // Boolean assertions
        $this->assertTrue(true);
        $this->assertFalse(false);
        
        // Null assertions
        $this->assertNull(null);
        $this->assertNotNull('value');
        
        // Empty assertions
        $this->assertEmpty([]);
        $this->assertNotEmpty(['item']);
        
        // Type assertions
        $this->assertIsArray([]);
        $this->assertIsString('text');
        $this->assertIsInt(42);
        $this->assertIsBool(true);
        $this->assertIsFloat(3.14);
        
        // Instance assertions
        $user = new User();
        $this->assertInstanceOf(User::class, $user);
    }
    
    public function testStringAssertions(): void
    {
        $string = 'Hello World';
        
        $this->assertStringContains('World', $string);
        $this->assertStringNotContains('PHP', $string);
        $this->assertStringStartsWith('Hello', $string);
        $this->assertStringEndsWith('World', $string);
        $this->assertMatchesRegularExpression('/^Hello/', $string);
    }
    
    public function testArrayAssertions(): void
    {
        $array = ['apple', 'banana', 'cherry'];
        
        $this->assertContains('apple', $array);
        $this->assertNotContains('orange', $array);
        $this->assertCount(3, $array);
        $this->assertArrayHasKey('name', ['name' => 'John', 'age' => 30]);
        $this->assertArrayNotHasKey('email', ['name' => 'John', 'age' => 30]);
    }
    
    public function testFileAssertions(): void
    {
        $filename = 'test.txt';
        file_put_contents($filename, 'test content');
        
        $this->assertFileExists($filename);
        $this->assertFileIsReadable($filename);
        $this->assertFileIsWritable($filename);
        
        unlink($filename);
        $this->assertFileDoesNotExist($filename);
    }
}
?>

Advanced Assertions

<?php
class AdvancedAssertionsTest extends TestCase
{
    public function testObjectAssertions(): void
    {
        $user1 = new User('John', '[email protected]');
        $user2 = new User('John', '[email protected]');
        
        // Object equality (same properties)
        $this->assertEquals($user1, $user2);
        
        // Object identity (same instance)
        $this->assertNotSame($user1, $user2);
        $this->assertSame($user1, $user1);
        
        // Object properties
        $this->assertObjectHasAttribute('name', $user1);
        $this->assertObjectNotHasAttribute('invalidProperty', $user1);
    }
    
    public function testExceptionAssertions(): void
    {
        // Test that exception is thrown
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Invalid email format');
        $this->expectExceptionCode(1001);
        
        $validator = new EmailValidator();
        $validator->validate('invalid-email');
    }
    
    public function testOutputAssertions(): void
    {
        $this->expectOutputString('Hello World');
        echo 'Hello World';
    }
    
    public function testJsonAssertions(): void
    {
        $json = '{"name": "John", "age": 30}';
        $expected = ['name' => 'John', 'age' => 30];
        
        $this->assertJson($json);
        $this->assertJsonStringEqualsJsonString($json, json_encode($expected));
    }
    
    public function testCustomAssertions(): void
    {
        // Custom assertion for user validation
        $this->assertValidUser(new User('John', '[email protected]'));
    }
    
    private function assertValidUser(User $user): void
    {
        $this->assertNotEmpty($user->getName(), 'User name should not be empty');
        $this->assertMatchesRegularExpression('/^[^@]+@[^@]+\.[^@]+$/', $user->getEmail(), 'User email should be valid');
        $this->assertTrue($user->isActive(), 'User should be active');
    }
}
?>

Test Organization and Structure

Test Setup and Teardown

<?php
class UserRepositoryTest extends TestCase
{
    private PDO $pdo;
    private UserRepository $userRepository;
    
    protected function setUp(): void
    {
        // Runs before each test method
        $this->pdo = new PDO('sqlite::memory:');
        $this->createUserTable();
        $this->userRepository = new UserRepository($this->pdo);
    }
    
    protected function tearDown(): void
    {
        // Runs after each test method
        $this->pdo = null;
    }
    
    public static function setUpBeforeClass(): void
    {
        // Runs once before all tests in this class
        // Use for expensive setup operations
    }
    
    public static function tearDownAfterClass(): void
    {
        // Runs once after all tests in this class
        // Use for cleanup of class-level resources
    }
    
    private function createUserTable(): void
    {
        $this->pdo->exec('
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name VARCHAR(255) NOT NULL,
                email VARCHAR(255) UNIQUE NOT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ');
    }
    
    public function testCreateUser(): void
    {
        $user = $this->userRepository->create('John Doe', '[email protected]');
        
        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals('John Doe', $user->getName());
        $this->assertEquals('[email protected]', $user->getEmail());
        $this->assertNotNull($user->getId());
    }
    
    public function testFindUserById(): void
    {
        $createdUser = $this->userRepository->create('Jane Doe', '[email protected]');
        $foundUser = $this->userRepository->findById($createdUser->getId());
        
        $this->assertEquals($createdUser->getId(), $foundUser->getId());
        $this->assertEquals($createdUser->getName(), $foundUser->getName());
    }
    
    public function testFindNonExistentUserReturnsNull(): void
    {
        $user = $this->userRepository->findById(999);
        $this->assertNull($user);
    }
}
?>

Test Factories and Builders

<?php
class UserFactory
{
    public static function create(array $attributes = []): User
    {
        $defaults = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'age' => 30,
            'active' => true
        ];
        
        $attributes = array_merge($defaults, $attributes);
        
        return new User(
            $attributes['name'],
            $attributes['email'],
            $attributes['age'],
            $attributes['active']
        );
    }
    
    public static function createMany(int $count, array $attributes = []): array
    {
        $users = [];
        for ($i = 0; $i < $count; $i++) {
            $userAttributes = array_merge($attributes, [
                'email' => "user{$i}@example.com"
            ]);
            $users[] = self::create($userAttributes);
        }
        return $users;
    }
}

class UserBuilder
{
    private array $attributes = [];
    
    public function withName(string $name): self
    {
        $this->attributes['name'] = $name;
        return $this;
    }
    
    public function withEmail(string $email): self
    {
        $this->attributes['email'] = $email;
        return $this;
    }
    
    public function withAge(int $age): self
    {
        $this->attributes['age'] = $age;
        return $this;
    }
    
    public function inactive(): self
    {
        $this->attributes['active'] = false;
        return $this;
    }
    
    public function build(): User
    {
        return UserFactory::create($this->attributes);
    }
}

// Usage in tests
class UserServiceTest extends TestCase
{
    public function testUserCreationWithFactory(): void
    {
        $user = UserFactory::create(['name' => 'Test User']);
        $this->assertEquals('Test User', $user->getName());
    }
    
    public function testUserCreationWithBuilder(): void
    {
        $user = (new UserBuilder())
            ->withName('Builder User')
            ->withAge(25)
            ->inactive()
            ->build();
            
        $this->assertEquals('Builder User', $user->getName());
        $this->assertEquals(25, $user->getAge());
        $this->assertFalse($user->isActive());
    }
}
?>

Mocking and Test Doubles

Basic Mocking

<?php
class EmailServiceTest extends TestCase
{
    public function testSendWelcomeEmail(): void
    {
        // Create a mock of the email gateway
        $emailGateway = $this->createMock(EmailGateway::class);
        
        // Configure the mock's expectations
        $emailGateway->expects($this->once())
            ->method('send')
            ->with(
                $this->equalTo('[email protected]'),
                $this->equalTo('Welcome!'),
                $this->stringContains('Welcome to our service')
            )
            ->willReturn(true);
        
        // Test the service
        $emailService = new EmailService($emailGateway);
        $result = $emailService->sendWelcomeEmail('[email protected]', 'John');
        
        $this->assertTrue($result);
    }
    
    public function testSendEmailWithFailure(): void
    {
        $emailGateway = $this->createMock(EmailGateway::class);
        
        $emailGateway->expects($this->once())
            ->method('send')
            ->willThrowException(new EmailDeliveryException('SMTP server unavailable'));
        
        $this->expectException(EmailDeliveryException::class);
        
        $emailService = new EmailService($emailGateway);
        $emailService->sendWelcomeEmail('[email protected]', 'John');
    }
}

// Example implementation
class EmailService
{
    private EmailGateway $emailGateway;
    
    public function __construct(EmailGateway $emailGateway)
    {
        $this->emailGateway = $emailGateway;
    }
    
    public function sendWelcomeEmail(string $email, string $name): bool
    {
        $subject = 'Welcome!';
        $body = "Welcome to our service, {$name}!";
        
        return $this->emailGateway->send($email, $subject, $body);
    }
}

interface EmailGateway
{
    public function send(string $to, string $subject, string $body): bool;
}
?>

Advanced Mocking Techniques

<?php
class PaymentProcessorTest extends TestCase
{
    public function testProcessPaymentWithStub(): void
    {
        // Create a stub that always returns success
        $paymentGateway = $this->createStub(PaymentGateway::class);
        $paymentGateway->method('charge')
            ->willReturn(new PaymentResult(true, 'txn_123'));
        
        $processor = new PaymentProcessor($paymentGateway);
        $result = $processor->processPayment(100.0, 'card_token');
        
        $this->assertTrue($result->isSuccessful());
    }
    
    public function testProcessPaymentWithConditionalMock(): void
    {
        $paymentGateway = $this->createMock(PaymentGateway::class);
        
        // Different return values based on input
        $paymentGateway->method('charge')
            ->willReturnMap([
                [100.0, 'valid_token', new PaymentResult(true, 'txn_123')],
                [100.0, 'invalid_token', new PaymentResult(false, null, 'Invalid token')],
            ]);
        
        $processor = new PaymentProcessor($paymentGateway);
        
        // Test successful payment
        $result1 = $processor->processPayment(100.0, 'valid_token');
        $this->assertTrue($result1->isSuccessful());
        
        // Test failed payment
        $result2 = $processor->processPayment(100.0, 'invalid_token');
        $this->assertFalse($result2->isSuccessful());
    }
    
    public function testProcessPaymentWithCallback(): void
    {
        $paymentGateway = $this->createMock(PaymentGateway::class);
        
        // Use callback to generate dynamic return values
        $paymentGateway->method('charge')
            ->willReturnCallback(function ($amount, $token) {
                if ($amount > 1000) {
                    throw new PaymentException('Amount exceeds limit');
                }
                return new PaymentResult(true, 'txn_' . uniqid());
            });
        
        $processor = new PaymentProcessor($paymentGateway);
        
        // Test normal payment
        $result = $processor->processPayment(500.0, 'token');
        $this->assertTrue($result->isSuccessful());
        
        // Test payment exceeding limit
        $this->expectException(PaymentException::class);
        $processor->processPayment(1500.0, 'token');
    }
    
    public function testMultipleMethodMock(): void
    {
        $userRepository = $this->createMock(UserRepository::class);
        $logger = $this->createMock(Logger::class);
        
        // Configure multiple methods
        $userRepository->expects($this->once())
            ->method('findByEmail')
            ->with('[email protected]')
            ->willReturn(new User('John', '[email protected]'));
        
        $userRepository->expects($this->once())
            ->method('save')
            ->with($this->isInstanceOf(User::class));
        
        $logger->expects($this->once())
            ->method('info')
            ->with($this->stringContains('User updated'));
        
        $service = new UserService($userRepository, $logger);
        $service->updateUserName('[email protected]', 'John Doe');
    }
}
?>

Partial Mocks and Spies

<?php
class FileProcessorTest extends TestCase
{
    public function testProcessFileWithPartialMock(): void
    {
        // Create a partial mock - only mock specific methods
        $processor = $this->getMockBuilder(FileProcessor::class)
            ->onlyMethods(['validateFile'])
            ->getMock();
        
        $processor->expects($this->once())
            ->method('validateFile')
            ->willReturn(true);
        
        $result = $processor->processFile('/path/to/file.txt');
        $this->assertTrue($result);
    }
    
    public function testProcessFileWithSpy(): void
    {
        // Create a spy to track method calls
        $processor = $this->getMockBuilder(FileProcessor::class)
            ->onlyMethods(['logProcessing'])
            ->getMock();
        
        $processor->expects($this->exactly(2))
            ->method('logProcessing')
            ->withConsecutive(
                ['Starting file processing'],
                ['File processing completed']
            );
        
        $processor->processFile('/path/to/file.txt');
    }
}
?>

Test-Driven Development (TDD)

TDD Cycle Example

<?php
// Step 1: Write a failing test
class StringCalculatorTest extends TestCase
{
    public function testEmptyStringReturnsZero(): void
    {
        $calculator = new StringCalculator();
        $result = $calculator->add('');
        
        $this->assertEquals(0, $result);
    }
}

// Step 2: Write minimal code to pass
class StringCalculator
{
    public function add(string $numbers): int
    {
        return 0; // Minimal implementation
    }
}

// Step 3: Add more tests
class StringCalculatorTest extends TestCase
{
    public function testEmptyStringReturnsZero(): void
    {
        $calculator = new StringCalculator();
        $result = $calculator->add('');
        $this->assertEquals(0, $result);
    }
    
    public function testSingleNumberReturnsNumber(): void
    {
        $calculator = new StringCalculator();
        $result = $calculator->add('5');
        $this->assertEquals(5, $result);
    }
    
    public function testTwoNumbersReturnsSum(): void
    {
        $calculator = new StringCalculator();
        $result = $calculator->add('2,3');
        $this->assertEquals(5, $result);
    }
}

// Step 4: Refactor implementation
class StringCalculator
{
    public function add(string $numbers): int
    {
        if (empty($numbers)) {
            return 0;
        }
        
        $numbersArray = explode(',', $numbers);
        return array_sum(array_map('intval', $numbersArray));
    }
}
?>

Running Tests

Command Line Options

# Run all tests
./vendor/bin/phpunit

# Run specific test file
./vendor/bin/phpunit tests/Unit/CalculatorTest.php

# Run specific test method
./vendor/bin/phpunit --filter testAddition

# Run tests with coverage
./vendor/bin/phpunit --coverage-html coverage/

# Run tests in specific directory
./vendor/bin/phpunit tests/Unit/

# Run tests with verbose output
./vendor/bin/phpunit --verbose

# Run tests and stop on first failure
./vendor/bin/phpunit --stop-on-failure

# Run tests with custom configuration
./vendor/bin/phpunit --configuration phpunit.xml

# Run tests for specific group
./vendor/bin/phpunit --group integration

# Run tests with testdox output
./vendor/bin/phpunit --testdox

Test Groups and Annotations

<?php
class DatabaseTest extends TestCase
{
    /**
     * @group database
     * @group slow
     */
    public function testDatabaseConnection(): void
    {
        // Database test
    }
    
    /**
     * @group unit
     * @group fast
     */
    public function testCalculation(): void
    {
        // Unit test
    }
    
    /**
     * @requires PHP 8.0
     */
    public function testPHP8Feature(): void
    {
        // Test for PHP 8+ features
    }
    
    /**
     * @requires extension pdo
     */
    public function testPDOFeature(): void
    {
        // Test requiring PDO extension
    }
}
?>

Best Practices

Test Organization

<?php
// Good: Descriptive test names
class UserRegistrationTest extends TestCase
{
    public function testValidUserDataCreatesNewUser(): void
    {
        // Test implementation
    }
    
    public function testDuplicateEmailThrowsValidationException(): void
    {
        // Test implementation
    }
    
    public function testInvalidEmailFormatThrowsValidationException(): void
    {
        // Test implementation
    }
}

// Good: Use meaningful assertions
public function testUserCreation(): void
{
    $user = new User('John', '[email protected]');
    
    // Good: Specific assertions
    $this->assertEquals('John', $user->getName());
    $this->assertEquals('[email protected]', $user->getEmail());
    $this->assertTrue($user->isActive());
    
    // Avoid: Generic assertion
    // $this->assertTrue($user instanceof User);
}

// Good: Test one thing at a time
public function testEmailValidation(): void
{
    $validator = new EmailValidator();
    
    // Test only email validation
    $this->assertTrue($validator->isValid('[email protected]'));
}

public function testPasswordValidation(): void
{
    $validator = new PasswordValidator();
    
    // Test only password validation in separate test
    $this->assertTrue($validator->isValid('SecurePass123!'));
}
?>

Test Data Management

<?php
class TestDataHelper
{
    public static function createValidUser(array $overrides = []): array
    {
        return array_merge([
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'SecurePass123!',
            'age' => 30
        ], $overrides);
    }
    
    public static function createInvalidEmails(): array
    {
        return [
            'missing-at-symbol',
            '@missing-local-part.com',
            '[email protected]',
            'spaces [email protected]',
            '[email protected]'
        ];
    }
}

class UserValidationTest extends TestCase
{
    /**
     * @dataProvider invalidEmailProvider
     */
    public function testInvalidEmailsAreRejected(string $email): void
    {
        $userData = TestDataHelper::createValidUser(['email' => $email]);
        $validator = new UserValidator();
        
        $this->assertFalse($validator->validate($userData));
    }
    
    public function invalidEmailProvider(): array
    {
        return array_map(fn($email) => [$email], TestDataHelper::createInvalidEmails());
    }
}
?>

For more PHP testing concepts:

Summary

PHPUnit provides comprehensive testing capabilities for PHP applications:

  • Test Structure: Clear organization with setUp/tearDown methods
  • Assertions: Rich set of assertions for different data types and scenarios
  • Mocking: Powerful mocking system for testing in isolation
  • Test Organization: Groups, data providers, and configuration options
  • TDD Support: Excellent support for test-driven development practices
  • Integration: Works well with CI/CD pipelines and development workflows

Key principles for effective PHPUnit testing:

  • Write descriptive test names and organize tests logically
  • Use appropriate assertions for better error messages
  • Mock external dependencies to test in isolation
  • Follow the AAA pattern: Arrange, Act, Assert
  • Keep tests focused and independent
  • Use data providers for testing multiple scenarios

PHPUnit is essential for maintaining code quality and confidence in PHP applications, enabling developers to refactor and extend code safely while ensuring reliability and preventing regressions.