PHP Testing with PHPUnit
Before writing code
Fetch live docs: Web-search site:docs.phpunit.de phpunit 10 (or current version) for the latest PHPUnit documentation. Check https://phpunit.de/ for current version.
PHPUnit Fundamentals
Test Class Structure
- Test classes extend
PHPUnit\Framework\TestCase - Test methods prefixed with
testor annotated#[Test] setUp()— runs before each testtearDown()— runs after each testsetUpBeforeClass()/tearDownAfterClass()— per-class lifecycle
Assertions
Common assertions:
assertEquals($expected, $actual)— loose comparisonassertSame($expected, $actual)— strict comparison (type + value)assertTrue($value)/assertFalse($value)assertNull($value)/assertNotNull($value)assertInstanceOf($class, $object)assertCount($count, $array)assertArrayHasKey($key, $array)assertStringContainsString($needle, $haystack)expectException($class)— exception testing
Data Providers
Supply multiple test cases to a single test method:
#[DataProvider('providerName')]attribute (PHPUnit 10+)- Provider method returns array of arrays (each inner array = one test case)
- Reduces test duplication for parameterized testing
Test Doubles
Mocks
Verify behavior — assert that methods were called with expected arguments:
$this->createMock(SomeClass::class)$mock->expects($this->once())->method('save')->with($entity)$mock->expects($this->never())->method('delete')
Stubs
Provide canned responses — no behavior verification:
$stub->method('getById')->willReturn($entity)$stub->method('getList')->willReturn($searchResults)
Method Return Behaviors
willReturn($value)— always returns this valuewillReturnMap($map)— returns based on argument mappingwillReturnCallback($callable)— dynamic returnwillThrowException($exception)— throws on callwillReturnSelf()— returns the mock (for fluent APIs)
Consecutive Calls
willReturnOnConsecutiveCalls($val1, $val2, $val3) — different return per call.
Note:
willReturnOnConsecutiveCalls()is deprecated in PHPUnit 10.3+. UsewillReturn()with$this->onConsecutiveCalls()orwillReturnCallback()with a counter instead.
Testing Patterns
Arrange-Act-Assert (AAA)
- Arrange — set up test data, mocks, dependencies
- Act — call the method under test
- Assert — verify the result
Testing Exceptions
$this->expectException(NoSuchEntityException::class);
$this->expectExceptionMessage('Entity not found');
$repository->getById(999);
Testing Private/Protected Methods
Don't test private methods directly — test through public methods. If a private method is complex enough to test independently, it should probably be extracted to its own class.
Testing with Dependency Injection
Create the class under test with mocked dependencies:
- Create mocks for all constructor parameters
- Instantiate the class with mocks
- Configure mock behavior per test
- Call the method and assert
Code Coverage
--coverage-html <dir>— generate HTML coverage report--coverage-text— terminal output- Focus on meaningful coverage — high-value business logic
- Don't chase 100% — getters/setters and framework glue don't need coverage
PHPUnit Configuration
phpunit.xml or phpunit.xml.dist:
- Test suite directories
- Bootstrap file
- Coverage filters
- Environment variables
- Extensions
Best Practices
- One assertion per test (or one logical assertion group)
- Name tests descriptively:
testGetByIdThrowsWhenNotFound() - Use data providers for parameterized tests
- Mock external dependencies, not the class under test
- Keep tests fast — no I/O, no database, no network in unit tests
- Use
setUp()for common test setup - Test edge cases: null, empty, boundary values
- Write the test first (TDD) when practical
- Run tests before committing
Fetch PHPUnit docs for exact assertion methods, mock API, and configuration options for your PHPUnit version before writing tests.