Home » Php » php – Unit test for mocking a method called by new class object

php – Unit test for mocking a method called by new class object

Posted by: admin April 23, 2020 Leave a comment

Questions:

I am writing unit test for an existing code which is like this

class someClass {
    public function __construct() { ... }

    public function someFoo($var) {
        ...
        $var = "something";
        ...
        $model = new someClass();
        model->someOtherFoo($var);
    }

    public someOtherFoo($var){
         // some code which has to be mocked
    }
}

Here how should I be able to mock the call to function “someOtherFoo” such that it doesn’t execute “some code” inside someOtherFoo?

class someClassTest {
   public function someFoo() {
      $fixture = $this->getMock('someClass ', array('someOtherFoo'));
      $var = "something";
      ....
      // How to mock the call to someOtherFoo() here
   }

}

Is it possible to mock out the constructor so that it returns my own constructed function or variable?

Thanks

How to&Answers:

Wherever you have new XXX(...) in a method under test, you are doomed. Extract the instantiation to a new method–createSomeClass(...)–of the same class. This allows you to create a partial mock of the class under test that returns a stubbed or mock value from the new method.

class someClass {
    public function someFoo($var) {
        $model = $this->createSomeClass();  // call method instead of using new
        model->someOtherFoo($var);
    }

    public function createSomeClass() {  // now you can mock this method in the test
        return new someClass();
    }

    public function someOtherFoo($var){
         // some code which has to be mocked
    }
}

In the test, mock createSomeClass() in the instance on which you call someFoo(), and mock someOtherFoo() in the instance that you return from the first mocked call.

function testSomeFoo() {
    // mock someOtherFoo() to ensure it gets the correct value for $arg
    $created = $this->getMock('someClass', array('someOtherFoo'));
    $created->expects($this->once())
            ->method('someOtherFoo')
            ->with('foo');

    // mock createSomeClass() to return the mock above
    $creator = $this->getMock('someClass', array('createSomeClass'));
    $creator->expects($this->once())
            ->method('createSomeClass')
            ->will($this->returnValue($created));

    // call someFoo() with the correct $arg
    $creator->someFoo('foo');
}

Keep in mind that because the instance is creating another instance of the same class, two instances will normally be involved. You could use the same mock instance here if it makes it clearer.

function testSomeFoo() {
    $fixture = $this->getMock('someClass', array('createSomeClass', 'someOtherFoo'));

    // mock createSomeClass() to return the mock
    $fixture->expects($this->once())
            ->method('createSomeClass')
            ->will($this->returnValue($fixture));

    // mock someOtherFoo() to ensure it gets the correct value for $arg
    $fixture->expects($this->once())
            ->method('someOtherFoo')
            ->with('foo');

    // call someFoo() with the correct $arg
    $fixture->someFoo('foo');
}

Answer:

You can prefix your mock class name with overload:

Check out the docs on Mocking Hard Dependencies.

Your example would be something like:

/**
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
class SomeClassTest extends \PHPUnit\Framework\TestCase
{
    public function test_some_foo()
    {
        $someOtherClassMock = \Mockery::mock('overload:SomeOtherClass');
        $someOtherClassMock->shouldReceive('someOtherFoo')
            ->once()
            ->with('something')
            ->andReturn();

        $systemUnderTest = new SomeClass();

        $systemUnderTest->someFoo('something');
    }

}

I added the @runTestsInSeparateProcesses annotation because usually the mocked class will be used in other tests too. Without the annotation, then the autoloader will crash because of the class already exists error.

If this is the one and only place that mocked class is used in your test suite, then you should remove the annotation.

Answer:

I found my way here attempting to white-box test a class __constructor to make sure it calls a class method on itself, with some data passed in to the __constructor.

In case anyone else is here for the same reason, I thought I would share the method I ended up using (without the factory-style createSomeClass() method used in this question).

<?php
class someClass {

  public function __constructor($param1) {
    // here is the method in the constructor we want to call
    $this->someOtherFoo($param1);
  }

  public function someOtherFoo($var){  }

}

Now the PHPUnit test:

<?php
$paramData = 'someData';

// set up the mock class here
$model = $this->getMock('someClass', 
  array('someOtherFoo'), // override the method we want to check
  array($paramData) // we need to pass in a parameter to the __constructor
);

// test that someOtherFoo() is called once, with out test data
$model->expects($this->once())
      ->with($paramData)
      ->method('someOtherFoo');

// directly call the constructor, instead of doing "new someClass" like normal
$model->__construct($paramData);