Asked  9 Months ago    Answers:  5   Viewed   74 times

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.

 Answers

44

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());
    }   
}
Wednesday, March 31, 2021
 
Whakkee
answered 9 Months ago
100

For testing properties, I'd make the same arguments I make then talking about testing private methods.

You usually don't want to do this.

It's about testing observable behavior.

If you rename all your properties or decide to store them into an array you should not need to adapt your tests at all. You want your tests to tell you that everything still works! When you need to change the tests to make sure everything still works you lose all the benefits as you also could make an error changing the tests.

So, all in all, you lose the value of you test suite!


Just testing the get/set combinations would be ok enough but usually not every setter should have a getter and just creating them for testing is not a nice thing ether.

Usually, you set some stuff and then tell the method to DO (behavior) something. Testing for that (that the class does what is should do) is the best option for testing and should make testing the properties superfluous.


If you really want to do that there is the setAccessible functionality in PHP reflections API but I can't make up an example where I find this desirable

Finding unused properties to catch bugs / issues like this one:

The PHP Mess Detector As a UnusedPrivateField Rule

class Something
{
    private static $FOO = 2; // Unused
    private $i = 5; // Unused
    private $j = 6;
    public function addOne()
    {
        return $this->j++;
    }
}

This will generate two warnings for you because the variables are never accessed

Wednesday, March 31, 2021
 
Drazisil
answered 9 Months ago
49

You could just store up the failures for the end, say with a

$passing = true;
if (! false) { $passing = false; }
if (! true) { $passing = false; }
$this->assertTrue($passing);

but I highly discourage this form of testing. I have written tests like this, and they exponentially get out of hand, and worse, you start to get weird failures for hard-to-find reasons.

Additionally, smarter people than me agree, tests should not have any conditionals (if/else, try/catch), because each conditional adds significant complexity to the test. If a conditional is needed, perhaps both the test and the SUT, or System Under Test, should be looked at very carefully, for ways to make it simpler.

A much better way would be to change it to be two tests, and if they share a significant portion of the setup, then move those two tests into a new test class, with the shared setup performed in the Setup() method.

Saturday, May 29, 2021
 
RenegadeAndy
answered 7 Months ago
84

It may not make much difference in your case but the preferred approach is to mock interface, as normally if you follow TDD (Test Driven Development) then you could write your unit tests even before you write your implementation classes. Thus even if you did not have concrete class DataAccessImpl, you could still write unit tests using your interface DataAccess.

Moreover mocking frameworks have limitations in mocking classes, and some frameworks only mock interfaces by default.

Sunday, August 1, 2021
 
Bharanikumar
answered 4 Months ago
82

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

Thursday, August 12, 2021
 
shin
answered 4 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share