Learn how to test private and protected methods in PHPUnit. Use reflection and proper testing strategies to ensure private logic works as expected.
Introduction
Unit testing focuses on public methods since they define a class’s behavior. However, there are cases where private and protected methods need testing—especially when they contain complex logic.
Since private and protected methods are not accessible from outside the class, PHPUnit does not provide built-in support for testing them directly. However, using reflection and proxy methods, we can safely test these methods while following good testing practices.
This guide covers:
- Why testing private and protected methods is sometimes necessary
- Using PHP’s Reflection API to access private and protected methods
- Alternative approaches like exposing methods for testing
- Best practices to ensure maintainable unit tests
1. Should You Test Private and Protected Methods?
When to Avoid Testing Private Methods
- Private methods are implementation details and should not be tested directly.
- Unit tests should focus on public methods, which rely on private logic.
- Refactoring private methods often breaks tests unnecessarily.
When You Might Need to Test Them
- If the private method contains complex logic that is difficult to verify indirectly.
- If testing the method through public methods causes too much overhead.
- If the protected method is part of an extendable class used in multiple subclasses.
If possible, test private methods indirectly by calling the public methods that use them. If not, use reflection to test them directly.
2. Using Reflection to Test Private and Protected Methods
PHP’s Reflection API allows us to access private and protected methods for testing.
Example: Testing a Private Method with Reflection
Consider the following UserManager class with a private method:
<?php
namespace App;
class UserManager
{
private function formatEmail($email)
{
return strtolower(trim($email));
}
}
Since formatEmail()
is private, we cannot call it directly. Instead, we use reflection in PHPUnit to test it.
Unit Test Using Reflection
<?php
use PHPUnit\Framework\TestCase;
use App\UserManager;
use ReflectionMethod;
class UserManagerTest extends TestCase
{
public function testFormatEmail()
{
$userManager = new UserManager();
$reflection = new ReflectionMethod(UserManager::class, 'formatEmail');
$reflection->setAccessible(true);
$formattedEmail = $reflection->invoke($userManager, ' JOHN@EXAMPLE.COM ');
$this->assertEquals('john@example.com', $formattedEmail);
}
}
How It Works:
- Create a ReflectionMethod object for
formatEmail()
. - Set the method to be accessible using
setAccessible(true)
. - Invoke the method on the class instance and pass required arguments.
- Assert that the returned result is correct.
This approach allows us to bypass access restrictions while keeping the original class unchanged.
3. Testing Protected Methods with Reflection
Protected methods behave similarly to private methods but can be accessed in subclasses. We can still use reflection to test them.
Example: Testing a Protected Method
class User
{
protected function getUsername()
{
return 'JohnDoe';
}
}
Unit Test for a Protected Method
public function testGetUsername()
{
$user = new User();
$reflection = new ReflectionMethod(User::class, 'getUsername');
$reflection->setAccessible(true);
$username = $reflection->invoke($user);
$this->assertEquals('JohnDoe', $username);
}
The only difference between private and protected methods is that protected methods can be overridden in subclasses.
4. Using Proxy Methods Instead of Reflection
A better alternative to reflection is adding a protected proxy method in a subclass.
Modifying the Class for Easier Testing
class UserManagerProxy extends UserManager
{
public function testFormatEmail($email)
{
return $this->formatEmail($email);
}
}
Testing the Proxy Method
public function testFormatEmailUsingProxy()
{
$proxy = new UserManagerProxy();
$this->assertEquals('john@example.com', $proxy->testFormatEmail(' JOHN@EXAMPLE.COM '));
}
Why Use a Proxy Instead of Reflection?
- It avoids reflection overhead and keeps tests readable.
- It follows inheritance best practices for testing protected methods.
- It allows us to test in a more natural way while keeping encapsulation.
5. Indirectly Testing Private Methods Using Public Methods
If a private method is used within a public method, the best approach is to test the public method instead.
Example: Indirect Testing Through a Public Method
class UserManager
{
private function formatEmail($email)
{
return strtolower(trim($email));
}
public function isEmailValid($email)
{
$email = $this->formatEmail($email);
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
}
Unit Test for the Public Method
public function testIsEmailValid()
{
$userManager = new UserManager();
$this->assertTrue($userManager->isEmailValid(' John@Example.COM '));
$this->assertFalse($userManager->isEmailValid('invalid-email'));
}
Why Use This Approach?
- Tests both private and public logic together.
- Keeps tests focused on expected behavior.
- Avoids breaking tests when refactoring private methods.
6. Best Practices for Testing Private and Protected Methods
- Test private methods only when necessary—prefer testing through public methods.
- Use reflection sparingly—it breaks encapsulation and can make tests brittle.
- Consider using proxy subclasses for testing protected methods.
- Keep test cases maintainable—ensure tests don't break due to internal refactoring.
- Follow good OOP principles—if you need to test private methods often, consider refactoring your class.
Conclusion
While private and protected methods should not usually be tested directly, there are cases where testing them is useful.
This guide covered:
- When and why to test private or protected methods
- Using reflection to test private methods in PHPUnit
- Using proxy classes as a better alternative
- Testing private methods indirectly through public methods
- Best practices for writing maintainable tests
By following these techniques, you can effectively test internal logic while keeping your unit tests maintainable and reliable.