How do I mock a method called within the constructor in a PHP unit test?
Asked Answered
B

6

8

I'm having trouble unit testing a class that has a method called within the constructor. I don't understand how to mock this. Perhaps I should use the 'setUp' method of phpUnit?

I'm using the Mockery library. Is there is a better tool than this?

class ToTest
{

   function __construct() {

       $this->methodToMock(); // need to mock that for future tests 

   }

   // my methods class

}

Any suggestions would be appreciated.

Burkes answered 15/4, 2014 at 8:55 Comment(0)
M
9

If you class is difficult to instantiate to test, that is a code smell that your class is doing too much or doing work in the constructor.

http://misko.hevery.com/code-reviewers-guide/

Flaw #1: Constructor does Real Work

Warning Signs

  • new keyword in a constructor or at field declaration
  • Static method calls in a constructor or at field declaration
  • Anything more than field assignment in constructors
  • Object not fully initialized after the constructor finishes (watch out for initialize methods)
  • Control flow (conditional or looping logic) in a constructor
  • Code does complex object graph construction inside a constructor rather than using a factory or builder
  • Adding or using an initialization block

Whatever your methodToMock function does in your constructor needs to be rethought. As mentioned in the other answers, you probably want to use dependency injection to pass in things that your class is doing.

Rethink what your class is actually doing and refactor so that it is easier to test. This also has the benefit of making your class easier to reuse and modify later on.

Martella answered 15/4, 2014 at 22:3 Comment(1)
When working in a legacy code-base, it's not always possible or desirable to refactor code like this.Badinage
U
3

The problem here is that the method can not be mocked as the object is not yet instantiated. sectus answer is valid but maybe not very flexible, as it can be difficult to change the behavior of the mocked method on different tests.

You can create another class that does the same as the method you want to mock, and have an instance of that class passed as a constructor argument. That way you can pass a mock class on your test. Usually the problem you're having is a smell of a class doing too many things.

Untouchable answered 15/4, 2014 at 9:14 Comment(1)
Thanks for the answer! really appreciated, may you do a simple example of it? Thanks :)Burkes
G
2

To test this class, you would mock the internal object (methodToMock) and then use Dependency Injection to pass the mocked service instead of the real one.

Class:

class ToTest{
    private $svc;

    // Constructor Injection, pass the Service object here
    public function __construct($Service = NULL)
    {
        if(! is_null($Service) )
        {
            if($Service instanceof YourService)
            {
                $this->SetService($Service);
            }
        }
    }

    function SetService(YourService $Service)
    {
        $this->svc = $Service
    }

    function DoSomething($request) {
        $svc    = $this->svc;
        $result = $svc->getResult($request);        // Get Result from Real Service
        return $result;
    }

    function DoSomethingElse($Input) {
         // do stuff
         return $Input;
    }
}

Test:

class ServiceTest extends PHPUnit_Framework_TestCase
{
    // Simple test for DoSomethingElse to work Properly
    // Could also use dataProvider to send different returnValues, and then check with Asserts.
    public function testDoSomethingElse()
    {
        $TestClass = new YourService();
        $this->assertEquals(1, $TestClass->DoSomethingElse(1));
        $this->assertEquals(2, $TestClass->DoSomethingElse(2));
    }

    public function testDoSomething()
    {
        // Create a mock for the YourService class,
        // only mock the DoSomething() method. Calling DoSomethingElse() will not be processed
        $MockService = $this->getMock('YourService', array('DoSomething'));

        // Set up the expectation for the DoSomething() method 
        $MockService->expects($this->any())
                    ->method('getResult')
                    ->will($this->returnValue('One'));

        // Create Test Object - Pass our Mock as the service
        $TestClass = new ToTest($MockService);
        // Or
        // $TestClass = new ToTest();
        // $TestClass->SetService($MockService);

        // Test DoSomething
        $RequestString = 'Some String since we did not specify it to the Mock';  // Could be checked with the Mock functions
        $this->assertEquals('One', $TestClass->DoSomething($RequestString));
    }
}
Grisly answered 15/4, 2014 at 15:53 Comment(2)
Nice and clean explanation! Thanks! I will try that and I'll let know how is gone! Then the accepted answer will be your :)Burkes
No problem, hope it helps you out.Grisly
G
1

I was also wondering this which is how I found your question. In the end I decided to do something a little bit dirty... use reflection.

Here's the method I want to test:

/**
 * ArrayPool constructor.
 * @param array $tasks Things that might be tasks
 */
public function __construct(array $tasks)
{
    foreach ($tasks as $name => $parameters) {
        if ($parameters instanceof TaskInterface) {
            $this->addTask($parameters);
            continue;
        }
        if ($parameters instanceof DescriptionInterface) {
            $this->addTask(new Task($parameters));
            continue;
        }
        $this->addPotentialTask($name, $parameters);
    }
}

For the purposes of this test, I don't want to actually run ->addTask or ->addPotentialTask, only know that they would be called.

Here's the test:

/**
 * @test
 * @covers ::__construct
 * @uses \Foundry\Masonry\Core\Task::__construct
 */
public function testConstruct()
{
    $task = $this->getMockForAbstractClass(TaskInterface::class);
    $description = $this->getMockForAbstractClass(DescriptionInterface::class);
    $namedTask = 'someTask';
    $parameters = [];

    $arrayPool =
        $this
            ->getMockBuilder(ArrayPool::class)
            ->disableOriginalConstructor()
            ->setMethods(['addTask', 'addPotentialTask'])
            ->getMock();

    $arrayPool
        ->expects($this->at(0))
        ->method('addTask')
        ->with($task);
    $arrayPool
        ->expects($this->at(1))
        ->method('addTask')
        ->with($this->isInstanceOf(TaskInterface::class));
    $arrayPool
        ->expects($this->at(2))
        ->method('addPotentialTask')
        ->with($namedTask, $parameters);

    $construct = $this->getObjectMethod($arrayPool, '__construct');
    $construct([
        0=>$task,
        1=>$description,
        $namedTask => $parameters
    ]);
}

The magic happens in getObjectMethod which takes an object and returns a callable closure that will invoke the method on an instance of the object:

/**
 * Gets returns a proxy for any method of an object, regardless of scope
 * @param object $object Any object
 * @param string $methodName The name of the method you want to proxy
 * @return \Closure
 */
protected function getObjectMethod($object, $methodName)
{
    if (!is_object($object)) {
        throw new \InvalidArgumentException('Can not get method of non object');
    }
    $reflectionMethod = new \ReflectionMethod($object, $methodName);
    $reflectionMethod->setAccessible(true);
    return function () use ($object, $reflectionMethod) {
        return $reflectionMethod->invokeArgs($object, func_get_args());
    };
}

And I know the loop and the conditions all function correctly without going off into code I don't want to enter here.

enter image description here

TL;DR:

  1. Disble __construct
  2. Set up mocks
  3. Use reflection to call __construct after the object was instantiated
  4. Try not to lose any sleep over it
Gamine answered 11/2, 2016 at 14:24 Comment(0)
A
1
class ToTest
{
   function __construct(){
       $this->methodToMock(); // need to mock that for future tests 
   }
   // my methods class
    public function methodToMock(){}
}

class ToTestTest{
    /**
     * @test
     * it should do something
     */
    public function it_should_do_something(){
        $ToTest = \Mockery::mock('ToTest')
        ->shouldDeferMissing()
        ->shouldReceive("methodToMock")
        ->andReturn("someStub")
        ->getMock();

        $this->assertEquals($expectation, $ToTest->methodToMock());
    }
}
Assuntaassur answered 26/7, 2017 at 15:56 Comment(0)
D
0

Just extend this class and override your method if it public or protected.

Dumfries answered 15/4, 2014 at 9:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.