Closures, Anonymous Classes and an alternative approach to Test Mocking (Part 3)

I have heard people say that you shouldn’t test abstract classes or traits, only the concrete classes that implement or use them. I don’t follow that approach: unit testing is all about testing in isolation from anything that might affect those tests. Testing a concrete class that implements an abstract one, or uses a trait, means that the abstract class or trait is no longer fully isolated, but is being tested within the scope of the whole of that concrete class. We should still always test concrete classes as well; but we should also test the abstract classes and traits as individual units.

So just how do we test something that cannot be instantiated on its own?

I’ve seen some developers advocate having a separate “minimal” concrete class that is used purely for testing an abstract or a trait; but that’s really just polluting our codebase with classes that exist solely for the purpose of testing, and I don’t really believe taking that approach is a sensible idea, and nor is it necessary.

PHPUnit provides getMockForTrait() and getMockForAbstractClass() for precisely this purpose, generating a test double, and mocking any abstract methods of the specified trait or abstract class, then allowing us to test the concrete methods that they implement.

If we take a look at the example from the PHPUnit documentation, which tests the following class:


abstract class AbstractClass {
    public function concreteMethod() {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

and the corresponding test using PHPUnit’s getMockForAbstractClass():


class AbstractClassTest extends TestCase {
    public function testConcreteMethod() {
        $stub = $this->getMockForAbstractClass(AbstractClass::class);

        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

        $this->assertTrue($stub->concreteMethod());
    }
}

So we’re mocking the abstract method abstractMethod() to test concreteMethod(), and can do our asserts as normal.

In the previous two articles in this series, I’ve mentioned using Anonymous Classes for creating test doubles: as an alternative to the example from the PHPUnit Documentation, we can replicate that test using an Anonymous Class instead of PHPUnit’s  getMockForAbstractClass()method:


class AbstractClassTest extends TestCase {
    public function testConcreteMethod() {
        $stub = new class() extends AbstractClass {
            public function abstractMethod() {
                return true;
            }
        };

        $this->assertTrue($stub->concreteMethod());
    }
}

Here, we’re creating a minimal concrete class to extend the abstract that we’re testing, implementing the abstract methods required for testing; but it’s a “disposable” class, so it doesn’t clutter up our codebase with “real” concrete classes whose only purpose is for testing. Again, we do the asserts as normal.

Both approaches are pretty simple, and there is no clear benefit to using an Anonymous Class to create $stub in preference to PHPUnit’s native approach.


Testing an abstract class or a trait becomes a bit harder though if the concrete methods that we want to test have been defined as protected rather than public, because any concrete classes which extend that abstract are expected to provide the caller methods themselves.


abstract class AbstractClass {
    protected function concreteMethod() {
        return $this->abstractMethod();
    }

    protected abstract function abstractMethod();
}

This time, if we use getMockForAbstractClass() to create a mock, then concreteMethod() is still a protected method, and cannot be called directly in our test, so we cannot test it directly. One approach to resolve this problem is to create a Closure that can be used to call the protected method, and bind that to the mock so the reference to $this in the Closure is within the scope of the stub – a technique that I have written about previously  (although in that article I wrote about using it to access properties rather than call methods; but it is exactly the same principle). Then we can run the test by invoking the Closure.


class AbstractClassTest extends \PHPUnit_Framework_TestCase {
    public function testConcreteMethod() {
        $stub = $this->getMockForAbstractClass(AbstractClass::class);

        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

        // Define a closure that will call the protected method using "$this".
        $concreteMethodCaller = function() {
            return $this->concreteMethod();
        };
        // Bind the closure to the stub's scope.
        $concreteMethodCallerBoundToStub = $concreteMethodCaller->bindTo($stub, $stub);

        $this->assertTrue($concreteMethodCallerBoundToStub());
    }
}

which works perfectly well, but all adds a significant degree of complexity to the test code, even for testing such a simple abstract, and it is also harder to read and understand the test.

However, if we’ve chosen to use an Anonymous Class as our stub, we can build a public caller method directly into the class when we define it, then invoke that caller method to execute the test. As with the Closure that we created for getMockForAbstractClass(), we keep the code within the caller method to an absolute minimum to keep as much isolation from the code of the method that we’re actually testing as possible; to do no additional processing beyond that which is necessary to execute the call that we want. To my mind, it’s a far cleaner approach than binding a Closure.


class AbstractClassTest extends \PHPUnit_Framework_TestCase {
    public function testConcreteMethod() {
        $stub = new class() extends AbstractClass {
            protected function abstractMethod() {
                return true;
            }
            public function concreteMethodCaller() {
                return $this->concreteMethod();
            }
        };

        $this->assertTrue($stub->concreteMethodCaller());
    }
}

This gives us much simpler test code: there’s no need to create any Closures, no need to bind anything to the stub, and it’s easier to read and understand exactly what the test code is doing. So even with as simple an abstract as this, there are some clear benefits to using an Anonymous Class to build our own “disposable” test concrete class rather than using PHPUnit’s mock builder.

 

This entry was posted in PHP and tagged , , , , , . Bookmark the permalink.

1 Response to Closures, Anonymous Classes and an alternative approach to Test Mocking (Part 3)

  1. Pingback: PHP Annotated Monthly – October 2017 | PhpStorm Blog

Leave a comment