Mocking concrete method in abstract class using phpunit

11,166

Solution 1

There was a Pull Request for this 2 years ago, but the information never been added in the documentation : https://github.com/sebastianbergmann/phpunit-mock-objects/pull/49

You can pass your concrete method in an array in argument 7 of getMockForAbstractClass().

See the code : https://github.com/andreaswolf/phpunit-mock-objects/blob/30ee7452caaa09c46421379861b4128ef7d95e2f/PHPUnit/Framework/MockObject/Generator.php#L225

Solution 2

I override getMock() in my base test case to add in all abstract methods because you must mock them all anyway. You could do something similar with the builder no doubt.

Important: You cannot mock private methods.

public function getMock($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = TRUE, $callOriginalClone = TRUE, $callAutoload = TRUE) {
    if ($methods !== null) {
        $methods = array_unique(array_merge($methods, 
                self::getAbstractMethods($originalClassName, $callAutoload)));
    }
    return parent::getMock($originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload);
}

/**
 * Returns an array containing the names of the abstract methods in <code>$class</code>.
 *
 * @param string $class name of the class
 * @return array zero or more abstract methods names
 */
public static function getAbstractMethods($class, $autoload=true) {
    $methods = array();
    if (class_exists($class, $autoload) || interface_exists($class, $autoload)) {
        $reflector = new ReflectionClass($class);
        foreach ($reflector->getMethods() as $method) {
            if ($method->isAbstract()) {
                $methods[] = $method->getName();
            }
        }
    }
    return $methods;
}
Share:
11,166
CheeseSucker
Author by

CheeseSucker

(my about me is currently blank)

Updated on June 18, 2022

Comments

  • CheeseSucker
    CheeseSucker almost 2 years

    Are there any good ways to mock concrete methods in abstract classes using PHPUnit?

    What I've found so far is:

    • expects()->will() works fine using abstract methods
    • It does not work for concrete methods. The original method is run instead.
    • Using mockbuilder and giving all the abstract methods and the concrete method to setMethods() works. However, it requires you to specify all the abstract methods, making the test fragile and too verbose.
    • MockBuilder::getMockForAbstractClass() ignores setMethod().


    Here are some unit tests examplifying the above points:

    abstract class AbstractClass {
        public function concreteMethod() {
            return $this->abstractMethod();
        }
    
        public abstract function abstractMethod();
    }
    
    
    class AbstractClassTest extends PHPUnit_Framework_TestCase {
        /**
         * This works for abstract methods.
         */
        public function testAbstractMethod() {
            $stub = $this->getMockForAbstractClass('AbstractClass');
            $stub->expects($this->any())
                    ->method('abstractMethod')
                    ->will($this->returnValue(2));
    
            $this->assertSame(2, $stub->concreteMethod()); // Succeeds
        }
    
        /**
         * Ideally, I would like this to work for concrete methods too.
         */
        public function testConcreteMethod() {
            $stub = $this->getMockForAbstractClass('AbstractClass');
            $stub->expects($this->any())
                    ->method('concreteMethod')
                    ->will($this->returnValue(2));
    
            $this->assertSame(2, $stub->concreteMethod()); // Fails, concreteMethod returns NULL
        }
    
        /**
         * One way to mock the concrete method, is to use the mock builder,
         * and set the methods to mock.
         *
         * The downside of doing it this way, is that all abstract methods
         * must be specified in the setMethods() call. If you add a new abstract
         * method, all your existing unit tests will fail.
         */
        public function testConcreteMethod__mockBuilder_getMock() {
            $stub = $this->getMockBuilder('AbstractClass')
                    ->setMethods(array('concreteMethod', 'abstractMethod'))
                    ->getMock();
            $stub->expects($this->any())
                    ->method('concreteMethod')
                    ->will($this->returnValue(2));
    
            $this->assertSame(2, $stub->concreteMethod()); // Succeeds
        }
    
        /**
         * Similar to above, but using getMockForAbstractClass().
         * Apparently, setMethods() is ignored by getMockForAbstractClass()
         */
        public function testConcreteMethod__mockBuilder_getMockForAbstractClass() {
            $stub = $this->getMockBuilder('AbstractClass')
                    ->setMethods(array('concreteMethod'))
                    ->getMockForAbstractClass();
            $stub->expects($this->any())
                    ->method('concreteMethod')
                    ->will($this->returnValue(2));
    
            $this->assertSame(2, $stub->concreteMethod()); // Fails, concreteMethod returns NULL
        }
    }
    
  • FoolishSeth
    FoolishSeth almost 10 years
    How is this different from getMockForAbstractClass ?
  • David Harkness
    David Harkness almost 10 years
    @FoolishSeth Previous versions of PHPUnit did not allow you to specify additional concrete methods to mock. Since you cannot instantiate a mock object without mocking every abstract method, it seems reasonable to merge them into the list when calling getMock() et al.