39

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

Sumitk
  • 1,485
  • 6
  • 19
  • 31
  • What is it you're trying to achieve? Why would you want to mock `someFoo` rather than test it? – Daren Chandisingh Oct 13 '11 at 21:35
  • 1
    I am trying to test the function `test()`. For it to be unit testable I want to mock out the call to `someFoo()` so that I can test function `test` without depending on someFoo. Does that make sense to you or should I give another example? Thanks – Sumitk Oct 13 '11 at 22:01
  • 2
    I think I would test `one::someFoo()` and test `one::test()` following that; if you've already tested `one::someFoo()` it shouldn't matter that `one::test()` depends on it, should it? After all, that's what will happen in real use. I understand why you'd mock a database connection or HTTP Client, but I don't see the problem here really. You could add a static property `$mocking` and a `one::setMocking(true)`, then add `if (self::$mocking) return;` in the `one::someFoo()` method, turning the flag on or off from your test case. – Daren Chandisingh Oct 13 '11 at 22:15
  • You are correct. I can do that here. But the thing is this is legacy code so I dont know how many places I would have to change it. – Sumitk Oct 13 '11 at 22:20
  • Sumtik, testing legacy code never appears simple first-hand. It's mostly because legacy code has no tests. You're changing this, so it's some work. Start with one class and take a look how it works. – hakre Oct 14 '11 at 00:37

3 Answers3

41

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');
}
David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • Hi David, I have changed my code to make it more readable. I am sorry that I write the previous one in hurry. So right now I am doing what @Daren has suggested and my test are proper but I suspect that I will face this issue in future too so thought should ask here as I can't get any help from the framework. Thanks! – Sumitk Oct 20 '11 at 00:54
  • I would follow my first option of extracting the constructor call to a new method. I've updated my answer to show how that would work. – David Harkness Oct 20 '11 at 01:34
  • Thanks for your explanation David! I was also doing the same thing but was confused. I have created my own framework which takes as an input the name of the class and function name. I then use `reflection` to get all the functions in that class and remove the function under test from the list. I then create a mock with all the other functions so now I have a mock obj with all the other functions mocked. This way I am able to reduce the redundant lines in my test code. But was wondering if there is any way of doing it without changing the existing code. Well thanks for clearing up my doubt! – Sumitk Oct 20 '11 at 03:05
  • 2
    There's an issue with this approach. Actually _createSomeClass()_ isn't supposed to be public, cause it has nothing to do with the interface of the class and it exposes an internal detail of the implementation. In other words passing _new someClass()_ would be a nicer solution, but for some categories it also looks awful (e.g. for controllers in MVC that should be the basement level when models are born). – Alexander Palamarchuk Sep 17 '15 at 09:08
9

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.

Jeff Puckett
  • 37,464
  • 17
  • 118
  • 167
2

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);
thaddeusmt
  • 15,410
  • 9
  • 67
  • 67