How to Test PHP Code With PHPUnit
As a software developer, you know automated testing is essential for maintaining a high-quality codebase. But it can be daunting to get started, especially if you‘re new to the practice. In this in-depth guide, I‘ll walk you through everything you need to know to begin testing your PHP code with PHPUnit.
We‘ll cover what unit testing is and why it‘s important, how to install and use PHPUnit, the anatomy of a PHPUnit test, and tips for writing effective, maintainable tests. By the end, you‘ll be equipped to start shipping more robust and reliable PHP applications. Let‘s jump in!
What Is Unit Testing?
Before we get to PHPUnit itself, let‘s make sure we‘re on the same page about unit testing. A unit test verifies that an individual unit of code, usually a single function or method, behaves as expected in isolation from the rest of the system.
By testing the smallest pieces of functionality in your codebase, you can catch bugs early before they have a chance to cause bigger problems down the line. Well-written unit tests also serve as living documentation, demonstrating how to properly use your code. And they give you the confidence to refactor and optimize without worrying about accidentally breaking things.
In a typical development workflow, a developer writes unit tests alongside or even before the actual implementation code. As they make changes, they re-run the tests to instantly verify nothing has regressed. Many teams enforce a policy that all new code must have corresponding unit tests before being merged in.
While unit tests can‘t catch every possible bug, they‘re an essential first line of defense and allow you to focus manual QA efforts on more complex integration scenarios. In combination with other types of automated testing like integration tests and end-to-end tests, they help ensure your application works as expected.
Introducing PHPUnit
So how do you actually write unit tests in PHP? While you could certainly write your own vanilla PHPUnit test framework from scratch, in practice most developers use PHPUnit. PHPUnit is the de facto standard testing framework for PHP projects.
At its core, PHPUnit provides:
- A base TestCase class to extend when defining tests
- A set of assertion methods for verifying expected outcomes
- A test runner for executing tests and reporting results
- Utilities for test doubles, data providers, and cross-version compatibility
It‘s a feature-rich, extensible framework that has everything you need for testing PHP code. Let‘s see how to install it and get started writing some tests.
Installing PHPUnit
The recommended way to install PHPUnit is via Composer, PHP‘s standard package manager. Here‘s how it works:
First, make sure you have Composer installed globally or in your project. Then run this command to add PHPUnit to your project‘s dev dependencies:
composer require --dev phpunit/phpunit
This will install the latest version of PHPUnit and add an entry to your composer.json‘s require-dev section. That means PHPUnit will only be installed in development, not in production.
If you don‘t have an existing composer.json, you can create one interactively with:
composer init
Just follow the prompts, and when asked to define dev dependencies, type phpunit/phpunit.
With PHPUnit installed, you can now execute its binary like:
./vendor/bin/phpunit
This will run your tests, but first we need to actually write some! Let‘s see how.
Anatomy of a PHPUnit Test
PHPUnit has a few conventions for defining and discovering your tests:
- Test classes are named after the class they are testing, with "Test" appended. E.g. a test for the User class would be called UserTest.
- Test classes extend the PHPUnit\Framework\TestCase base class.
- Individual test methods are public and named starting with "test". E.g. testCreatingUser().
- Assertions methods like assertEquals, assertSame, assertTrue are used to verify the code behaves as expected.
- Tests are stored in a separate tests/ directory, mirroring the structure of the src/ directory containing the implementation code.
Here‘s what a simple UserTest might look like:
<?php
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function testCreatingUser()
{
$user = new User("John Doe", "[email protected]");
$this->assertEquals("John Doe", $user->getName());
$this->assertEquals("[email protected]", $user->getEmail());
}
}
?>
This test class has a single test method that verifies the constructor sets the name and email properties correctly. It creates a new User instance and then makes assertions on the values returned by its getName() and getEmail() methods.
You‘d save this in a file like tests/UserTest.php. Let‘s see how to actually run it!
Running Your Tests
With your test class in place, you can ask PHPUnit to run it:
./vendor/bin/phpunit tests/UserTest.php
PHPUnit will load the UserTest class, execute any methods beginning with "test", and report the results. If all the assertions passed, you‘ll see output like:
OK (1 test, 2 assertions)
This indicates the single test method ran and made two assertions, both of which passed.
If an assertion fails, you‘ll see a descriptive error message pinpointing the problem:
1) UserTest::testCreatingUser
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-‘John Doe‘
+‘Jane Doe‘
From this, you can see the actual name value "Jane Doe" did not equal the expected value "John Doe", indicating a bug to fix.
You can also run all tests in the tests directory at once:
./vendor/bin/phpunit tests
PHPUnit will recursively look for *Test.php files and run them. This is handy as your test suite grows.
The PHPUnit command has many other useful options worth exploring, like:
- –filter to only run tests matching a pattern
- –testsuite to only run tests in a certain suite
- –coverage-html to generate an HTML code coverage report
Run ./vendor/bin/phpunit –help to see the full list of configuration options.
Testing a Complete Class
Let‘s flesh out our UserTest to cover more functionality. Consider this simple User class:
class User
{
protected $name;
protected $email;
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
}
public function getName()
{
return $this->name;
}
public function getEmail()
{
return $this->email;
}
public function getEmailVariables()
{
return [
‘full_name‘ => $this->name,
‘email‘ => $this->email
];
}
}
In addition to the constructor, it has methods for getting the name, email, and an array of data for email templates.
Here‘s how we could unit test it:
class UserTest extends TestCase
{
protected $user;
protected function setUp(): void
{
$this->user = new User("John Doe", "[email protected]");
}
protected function tearDown(): void
{
unset($this->user);
}
public function testGetName()
{
$this->assertEquals("John Doe", $this->user->getName());
}
public function testGetEmail()
{
$this->assertEquals("[email protected]", $this->user->getEmail());
}
public function testGetEmailVariables()
{
$emailVariables = $this->user->getEmailVariables();
$this->assertArrayHasKey(‘full_name‘, $emailVariables);
$this->assertArrayHasKey(‘email‘, $emailVariables);
$this->assertEquals(
[
‘full_name‘ => ‘John Doe‘,
‘email‘ => ‘[email protected]‘
],
$emailVariables
);
}
}
Some key things to note:
- We‘re now storing the User instance as a class property $this->user. This lets us share it between test methods.
- The setUp() method is called before each test method. It‘s used to put the class in a known state for testing.
- The tearDown() method is called after each test method. It‘s used to clean up any lingering test state.
- We added tests for each public method. This gives us 100% test coverage.
- The testGetEmailVariables() method shows more assertion examples, like assertArrayHasKey() to check for array keys and comparing the entire returned array to an expected one.
With disciplined unit testing like this, you can be confident your User class is solid and refactoring won‘t unwittingly break it.
Going Further
We‘ve only scratched the surface of PHPUnit‘s capabilities. As you write more tests, look into:
- Data providers for testing methods with multiple inputs
- Test doubles for replacing hard-to-test dependencies with pretend objects
- Skipping or marking tests incomplete with @annotations
- Defining custom assertions for project-specific logic
Also consider adopting the practice of test-driven development (TDD). With TDD, you actually write your tests before your implementation code. This forces you to design classes focused on how they‘ll be used. It also keeps you focused on just the required functionality.
The workflow looks like:
- Write a failing test for the simplest piece of functionality
- Write only enough implementation code to make the test pass
- Refactor the code, relying on the test for safety
- Repeat by writing the next test
This "red-green-refactor" cycle tends to produce more testable, maintainable code than writing tests after the fact. Give it a try!
Wrapping Up
In this guide, we covered what unit testing is and why it‘s critical for PHP developers. You learned how to install PHPUnit via Composer, how to write tests using its conventions, and how to run them to verify your code‘s correctness.
We walked through testing an example User class, demonstrating common assertions and the PHPUnit test lifecycle. Finally, we touched on some next steps for learning more about PHPUnit‘s advanced features and the practice of test-driven development.
I hope you now feel empowered to start unit testing your PHP code. It can be challenging at first, but stick with it. In time, you‘ll question how you ever got by without automated tests. Your future self will thank you!