Simplify your tests with anonymous classes
Published by Matthew Daly at 20th October 2018 1:48 pm
Anonymous classes were added in PHP7, but so far I haven't made all that much use of them. However, recently I've been working on building a simple dependency injection container for learning purposes. This uses the PHP Reflection API to determine how to resolve dependencies. For instance, if it's asked for a class for which one of the dependencies required by the constructor is an instance of the DateTime
class, it should create an instance, and then pass it into the constructor automatically when instantiating the class. Then it should return the newly created class.
Mocking isn't really a suitable approach for this use case because the container needs to return a concrete class instance to do its job properly. You could just create a series of fixture classes purely for testing purposes, but that would mean either defining more than one class in a file (violating PSR-2), or defining a load of fixture classes in separate files, meaning you'd have to write a lot of boilerplate, and you'd have to move between several different files to understand what's going on in the test.
Anonymous classes allow you a means to write simple classes for tests inline, as in this example for retrieving a very basic class. The tests use PHPSpec:
1<?php23namespace spec\Vendor\Package;45use Vendor\Package\MyClass;6use PhpSpec\ObjectBehavior;7use Prophecy\Argument;8use DateTime;910class MyClassSpec extends ObjectBehavior11{12 function it_can_resolve_registered_dependencies()13 {14 $toResolve = new class {15 };16 $this->set('Foo\Bar', $toResolve);17 $this->get('Foo\Bar')->shouldReturnAnInstanceOf($toResolve);18 }19}
You can also define your own methods inline. Here we implement the invoke()
magic method so that the class is a callable:
1<?php23class MyClassSpec extends ObjectBehavior4{5 function it_can_resolve_registered_invokable()6 {7 $toResolve = new class {8 public function __invoke() {9 return new DateTime;10 }11 };12 $this->set('Foo\Bar', $toResolve);13 $this->get('Foo\Bar')->shouldReturnAnInstanceOf('DateTime');14 }15}
You can also define a constructor. Here, we're getting the class name of a newly created anonymous class that accepts an instance of DateTime
as an argument to the constructor. Then, we can resolve a new instance out of the container:
1<?php23class MyClassSpec extends ObjectBehavior4{5 function it_can_resolve_dependencies()6 {7 $toResolve = get_class(new class(new DateTime) {8 public $datetime;9 public function __construct(DateTime $datetime)10 {11 $this->datetime = $datetime;12 }13 });14 $this->set('Foo\Bar', $toResolve);15 $this->get('Foo\Bar')->shouldReturnAnInstanceOf($toResolve);16 }17}
For classes that will extend an existing class or implement an interface, you can define those inline too. Or you can include a trait:
1<?php23class MyClassSpec extends ObjectBehavior4{5 function it_can_resolve_dependencies()6 {7 $toResolve = get_class(new class(new DateTime) extends Foo implements Bar {8 public $datetime;9 public function __construct(DateTime $datetime)10 {11 $this->datetime = $datetime;12 }1314 use MyTrait;15 });16 $this->set('Foo\Bar', $toResolve);17 $this->get('Foo\Bar')->shouldReturnAnInstanceOf($toResolve);18 }19}
In cases where the functionality is contained in a trait or abstract class, and you might need to add little or no additional functionality, this is a lot less verbose than creating a class the conventional way.
None of this is stuff you can't do without anonymous classes, but by defining these sort of disposable fixture classes inline in your tests, you're writing the minimum amount of code necessary to implement your test, and it's logical to define it inline since it's only ever used in the tests. One thing to bear in mind is that anonymous classes are created and instantiated at the same time, so you can't easily create a class and then instantiate an instance of it separately. However, you can instantiate one, then use the get_class()
function to get its class name and use that to resolve it, which worked well for my use case.
Another use case for anonymous classes is testing traits or abstract classes. I generally use Mockery as my mocking solution with PHPUnit tests, but I've sometimes missed the getMockForTrait()
method from PHPUnit. However, another option is to instantiate an anonymous class that includes that trait for testing purposes:
1<?php23$item = new class() {4 use MyTrait;5};
This way, your test class is as minimal as possible, and you can test the trait/abstract class in a fairly isolated fashion.