Lightweight PHP package that let you test singleton classes and inaccessible (private or protected) instance members.
composer require make/accessible
- Call inaccessible methods
- Set values into inaccessible properties
- Get values from inaccessible properties
- Instantiate singleton classes
Suppose you have the class bellow:
<?phpusePerson;
/** * Class PeopleGreeter. * * Greats a list of persons, nothing more. */class PeopleGreeter
{
/** @var Person[] */protected$peopleToGreet = [];
/** * Adds a person to `$peopleToGreat`. */publicfunctionaddPersonToGreet(Person$person)
{
$this->peopleToGreet[] = $person;
}
/** * Greats a `$person`. * * @param Person $person Person to great. */protectedfunctiongreet(Person$person)
{
if ($person->isWoman()) {
return'Hi Mrs. ' . $person->name();
} else {
return'Hello Mr. ' . $person->name();
}
}
/** * Greats everyone on `$peopleToGreat`. */publicfunctiongreetEveryone()
{
foreach ($this->peopleToGreetas$person) {
echo$this->greet($person);
}
}
}This class is correct, it does'nt exposes its inner elements (encapsulation) and was designed to do what is supposed to do.
But, how are you going to test this class? Test if the addPersonToGreet() method is really adding $person into $peopleToGreet and greet() method is really distinguishing mens from womens?
You can just make all your class members public, but that way you're breaking encapsulation, thats not good.
So, what can be done here?
One way to test addPersonToGreet() would be by directly accessing $peopleToGreet and check if $person is really there:
$person = newPerson('John Doe');
$peopleGreeter = newPeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$this->assertTrue(in_array($person, $peopleGreeter->peopleToGreet));One way to test greet() would be by directly accessing it:
$person = newPerson('John Doe');
$this->assertEquals("Hello Mr. John Doe", (newPeopleGreeter())->greet($person));But of course these methods will fail, its will result in:
Error: Cannot access protected property PeopleGreeter::$peopleToGreet.
Error: Cannot access protected method PeopleGreeter::greet().
because we can't access protected and private members outside of they parent scope.
Another way that works only with protected members is by extending their parent to another class that exposes its methods:
class ExtendedPeopleGreeter extends PeopleGreeter
{
publicfunctiongreet($person)
{
returnparent::greet($person);
}
publicfunctiongetPeopleToGreet()
{
returnparent::$peopleToGreet;
}
}$person = newPerson('John Doe');
$this->assertEquals("Hello Mr. John Doe", (newExtendedPeopleGreeter())->greet($person));$person = newPerson('John Doe');
$peopleGreeter = newExtendedPeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$this->assertTrue(in_array($person, $peopleGreeter->getPeopleToGreet()));Pros:
- You can test protected members.
Cons:
- Don't works with private members.
- Makes really hard write simples tests.
- You need to write a fake class every time you want to test a real class.
- You need to write a method for each inaccessilble member you want to test.
But of course there's another solution: Using PHP Reflection:
$person = newPerson('John Doe');
$peopleGreeter = newPeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$reflectedClass = new \ReflectionClass($peopleGreeter);
$propertyName = 'peopleToGreet';
if ($reflectedClass->hasProperty($propertyName)) {
$property = $reflectedClass->getProperty($propertyName);
$property->setAccessible(true);
$this->assertTrue(in_array($person, $property->getValue($peopleGreeter)));
}$person = newPerson('John Doe');
$reflectedClass = new \ReflectionClass($peopleGreeter);
$propertyName = 'peopleToGreet';
$peopleGreeter = newPeopleGreeter();
$reflectedClass = new \ReflectionClass($peopleGreeter);
$methodName = 'greet';
if ($reflectedClass->hasMethod($methodName)) {
$method = $reflectedClass->getMethod($methodName);
$method->setAccessible(true);
$returnedValue = $method->invokeArgs($peopleGreeter, ['person' => $person]);
$this->assertEquals("Hello Mr. John Doe", $returnedValue);
}Pros:
- Works for both protected and private members.
Cons:
- You need to write a lot of extra code to make simple tests.
- You need to write code that also need to be tested.
- Its hard to reuse this code in other test either in other projects.
We're testing only 2 methods, now think about 30 or 50??? No, no... Forget about it!
$person = newPerson('John Doe');
$peopleGreeter = newPeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$accessiblePeopleGreeter = Make::accessible($peopleGreeter);
$this->assertTrue(in_array($person, $accessiblePeopleGreeter->peopleToGreet));$person = newPerson('John Doe');
$peopleGreeter = newPeopleGreeter();
$accessiblePeopleGreeter = Make::accessible($peopleGreeter);
$this->assertEquals("Hello Mr. John Doe", $accessiblePeopleGreeter->greet($person));Did you saw that??? Just one line of code and we made our tests!!!
We gain access to our PeopleGreeter::class inaccessible members in a common and friendly way! ;)
Pros:
- User friendly.
- Just one line of code.
- Makes tests really easy.
- Works for both private and protected members.
- Don't require write fake classes or methods for each test.
- Encourages the usage of encapsulation in projects providing a more isolated and flexible environment.
Cons:
- 😅
As you can see, with Make/Accessible you can now make your projects encapsulated withour fear!
And you can also start testing singleton classes using Make/Accessible. Feature documentation soon.
We highly recommend the use of this package for tests purposes only.
Avoid use this package to gain access to encapsulated classes, since it's like break the door of someone's house that does'nt want you to get inside.
If you're testing the same class the same way, we recommend create a function getAccessibleInstance() at the bottom of your test class and there you make the instantiation, mocks, everything you need to instantiate the class you need to test:
class PeopleGreeterTest extends TestCase
{
publicfunctiontestGreet()
{
$person = newPerson('John Doe');
$peopleGreeter = $this->getAccessibleInstance();
$this->assertEquals("Hello Mr. John Doe", $peopleGreeter->greet($person));
}
Others test functions...
publicfunctiongetAccessibleInstance()
{
$instance = newPeopleGreeter();
return Make::accessible($instance);
}
}For more tips about best practices, please, read our best practices documentation.
Need a new feature? Found a bug? Please open a new issue or send me an email and we'll fix it as soon as possible.
Feel free to contribute forking, making changes and pull requests.
- Eleandro Duzentos and contributors.
The MakeAccessible is licensed under the MIT license. See the license file for more information.