Testing in PHP has matured significantly. PHPUnit 10 introduced a cleaner API and dropped legacy baggage. Pest brought an expressive, closure-based syntax that many teams prefer. Code coverage tools are faster and more accurate. And CI integration is straightforward with any provider.
This guide covers practical PHP testing in 2026—not theoretical best practices, but the patterns that work in production codebases.
PHPUnit 10: what changed
PHPUnit 10 was a major cleanup release. It removed methods that were deprecated in 9.x and introduced a simpler, more consistent API.
Key changes from PHPUnit 9
1 | // PHPUnit 9 (deprecated) |
Minimal PHPUnit setup
1 | <!-- phpunit.xml --> |
1 | composer require --dev phpunit/phpunit:^10 |
Pest: expressive PHP testing
Pest is built on PHPUnit but uses a closure-based syntax inspired by Jest (JavaScript) and RSpec (Ruby). It produces more readable tests with less boilerplate.
Pest vs PHPUnit syntax
1 | // PHPUnit |
Pest expectations
Pest’s expect() API is chainable and readable:
1 | test('order total calculation', function () { |
Pest datasets
1 | dataset('valid emails', [ |
Pest architecture testing
Pest includes architecture tests that enforce structural rules:
1 | arch('models should extend base model') |
Unit testing patterns
Testing pure functions
1 | // Simple, deterministic tests |
Testing with mocks
1 | test('order service calculates shipping', function () { |
Testing exceptions
1 | test('rejects negative quantities', function () { |
Integration testing
Integration tests verify that components work together. For PHP web applications, this means testing HTTP endpoints with a real database.
Laravel feature tests
1 | test('authenticated user can create article', function () { |
Database testing strategies
1 | // Option 1: RefreshDatabase (migrate fresh for each test class) |
DatabaseTransactions is fastest because it rolls back instead of dropping/recreating tables. RefreshDatabase is safest because it starts from a clean state.
Code coverage
Setting up coverage
1 | # With Xdebug |
Coverage configuration
1 | <!-- phpunit.xml --> |
What to measure
- Line coverage: Which lines were executed. The most common metric.
- Branch coverage: Which branches (if/else, switch) were taken. More meaningful than line coverage.
- Path coverage: Which paths through functions were taken. Most thorough but slowest.
Coverage targets
| Code type | Target | Why |
|---|---|---|
| Business logic | 90%+ | Core value, must be correct |
| Controllers | 80%+ | Integration tests cover these |
| Models/DTOs | 70%+ | Often simple, high coverage is easy |
| Config/bootstrap | Skip | Not worth testing |
CI integration
GitHub Actions
1 | # .github/workflows/tests.yml |
Common mistakes
Testing implementation instead of behavior: Tests should verify what a function does, not how it does it. If refactoring breaks tests that verify internal method calls, those tests are too coupled.
Skipping test data cleanup: Tests that leave data behind cause flaky failures in other tests. Use database transactions or RefreshDatabase.
Mocking everything: If a unit test mocks every dependency, it only tests that you configured the mocks correctly. Use real implementations when practical.
Ignoring slow tests: A test suite that takes 10 minutes gets run less often. Profile your tests, optimize the slow ones, and parallelize.
FAQ
Should I use PHPUnit or Pest?
Both work well. Pest has cleaner syntax and architecture testing. PHPUnit has wider ecosystem support and more documentation. New projects tend to prefer Pest; existing codebases can mix both.
What code coverage percentage should I target?
80% overall is a reasonable target. 100% is usually not worth the effort—the last 20% covers edge cases that are expensive to test and unlikely to fail.
How do I test private methods?
You don’t. Test the public interface that calls the private method. If a private method is complex enough to need its own tests, extract it into a separate class.
Next steps
Start by running composer require --dev pestphp/pest and writing three tests: one unit test, one feature test, and one architecture test. Then add coverage reporting to your CI pipeline.
For testing PHP APIs specifically, the REST API best practices guide includes API testing patterns. For managing test dependencies with Composer, ensure your test frameworks are in require-dev and always run composer install --no-dev in production.