Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHPUnit: include class after mocking it

I'm happily writing unit tests, but they clash when I run them all together. I'm testing this class:

class MyClass {

    public function sayHello() {
        return 'Hello world';
    }
}

using this test. All tests have a structure like this:

class MyClassTest extends PHPUnit_Framework_TestCase {
    private $subject;

    public static function setUpBeforeClass() {
        require_once('path/to/MyClass.php');
    }

    public function setUp() {
        $this->subject = new MyClass();
    }

    public function testSayHello() {
        $this->assertEquals('Hello world', $this->subject->sayHello());
    }
}

MyClassTest runs fine on its own. But PHPUnit will crash because I redeclare the class, if another test mocks MyClass and happens to run first:

class Inject {

    private $dependency;

    public function __construct(MyClass $myClass) {
        $this->dependency = $myClass;
    }

    public function printGreetings() {
        return $this->dependency->sayHello();
    }
}

class InjectTest extends PHPUnit_Framework_TestCase {

    public function testPrintGreetings() {
        $myClassMock = $this
            ->getMockBuilder('MyClass')
            ->setMethods(array('sayHello'))
            ->getMock();
        $myClassMock
            ->expects($this->once())
            ->method('sayHello')
            ->will($this->returnValue(TRUE));

        $subject = new Inject($myClassMock);

        $this->assertEquals('Hello world', $subject->printGreetings());
    }
}

I do use a bootstrap.php to fake some global functions not yet refactored.

I have no auto loaders and don't want to process-isolate EVERY test, because it takes forever. I tried inserting combinations @runTestsInSeparateProcesses and @preserveGlobalState enabled/disabled in the docblocks of both Test 1 & 2, I still get the same error.

like image 927
PeerBr Avatar asked Sep 05 '14 19:09

PeerBr


1 Answers

To understand this behaviour, you need to have a look at how PHPUnit works. getMockBuilder()->getMock(), dynamically creates the following code for the mock class:

class Mock_MyClass_2568ab4c extends MyClass implements PHPUnit_Framework_MockObject_MockObject
{
    private static $__phpunit_staticInvocationMocker;
    private $__phpunit_invocationMocker;

    public function __clone()
    {
        $this->__phpunit_invocationMocker = clone $this->__phpunit_getInvocationMocker();
    }

    public function sayHello()
    {
        $arguments = array();
        $count     = func_num_args();

        if ($count > 0) {
            $_arguments = func_get_ ... 

     # more lines follow ...

If MyClass hasn't already been loaded at this time, it adds the following dummy declaration:

class MyClass
{
}

This code will then getting parsed using eval() (!).

Since PHPUnit will execute InjectTest before MyClassTest, this means that the mock builder will define the dummy class and MyClass is already defined when MyClassTest::setUpBeforeClass will get called. That's why the error. I hope I could explain. Otherwise, dig into PHPUnit's code.


Solution:

Drop the setUpBeforeClass() method and put the require_once statement on top of the tests. setUpBeforeClass() is not meant for including classes. Refer to the docs.

Btw, having require_once on top will work because PHPUnit will include every test file before starting the first test.

tests/MyClassTest.php

require_once __DIR__ . '/../src/MyClass.php';

class MyClassTest extends PHPUnit_Framework_TestCase {
    private $subject;

    public function setUp() {
        $this->subject = new MyClass();
    }

    public function testSayHello() {
        $this->assertEquals('Hello world', $this->subject->sayHello());
    }
}

tests/InjectTest.php

require_once __DIR__ . '/../src/Inject.php';

class InjectTest extends PHPUnit_Framework_TestCase {

    public function testPrintGreetings() {
        $myClassMock = $this
            ->getMockBuilder('MyClass')
            ->setMethods(array('sayHello'))
            ->getMock();
        $myClassMock
            ->expects($this->once())
            ->method('sayHello')
            ->will($this->returnValue(TRUE));

        $subject = new Inject($myClassMock);

        $this->assertEquals(TRUE, $subject->printGreetings());
    }   
}
like image 141
hek2mgl Avatar answered Nov 07 '22 04:11

hek2mgl