Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to mock a class and override a method

I have this class I'm testing that's using Redis somewhere deeper:

<?php

class Publisher {
    function publish($message) {
        Redis::publish($message);
    }
}

class Foo {
    public function publishMessage() {
        $message = $this->generateMessage();
        $this->publish($message);
    }

    private function publish($message) {
        $this->getPublisher()->publish($message);
    }

    // below just for testing
    private $publisher;

    public function getPublisher() {
        if(empty($this->publisher) {
             return new Publisher();
        }
        return $this->publisher;
    }

    public function setPublisher($publisher) {
        $this->publisher = $publisher;
    }
}

Now I don't know how to test this. Of course I don't want to test Redis. What I actually need to test is if the message send to Redis is what I expect. (I think) I could write a function that returns the message and is public. But I don't like that idea. Here in this example I made it possible to set the publisher so while testing I can just return another Publisher class. Instead of sending the message it would save it internally so I can later assert it.

class Publisher {
    public $message;
    function publish($message) {
        $this->message = $message;
    }
}  

But then I don't know how to mock the Publisher class to change the method. Or I would have to inherit from the Publisher class. Also this way my tested class must contain code just for testing. Which I don't like either.

How would I test this properly? A mocking library for Redis exists but does not support publish.

like image 685
steros Avatar asked Feb 23 '18 08:02

steros


1 Answers

Some options described as methods of test class

class FooTest extends PHPUnit_Framework_TestCase // or PHPUnit\Framework\TestCase for version
{

    /**
     * First option: with PHPUnit's MockObject builder.
     */
    public function testPublishMessageWithMockBuilder() {
        // Internally mock builder creates new class that extends your Publisher
        $publisherMock = $this
            ->getMockBuilder(Publisher::class)
            ->setMethods(['publish'])
            ->getMock();

        $publisherMock
            ->expects($this->any()) // how many times we expect our method to be called
            ->method('publish') // which method
            ->with($this->exactly('your expected message')) // with what parameters we expect method "publish" to be called
            ->willReturn('what should be returned');
        $testedObject = new Foo;
        $testedObject->setPublisher($publisherMock);
        $testedObject->publish();
    }

    /**
     * Second option: with Prophecy
     */
    public function testPublishMessageWithProphecy() {
        // Internally prophecy creates new class that extends your Publisher
        $publisherMock = $this->prophesize(Publisher::class);

        // assert that publish should be called with parameters
        $publisherMock
            ->publish('expected message')
            ->shouldBeCalled();

        $testedObject = new Foo;
        $testedObject->setPublisher($publisherMock->reveal());
        $testedObject->publish();
    }

    /**
     * Third wierd option: with anonymous class (php version >= 7)
     * I am not recommend do something like that, its just for example
     */
    public function testFooWithAnonymousClass()
    {
        // explicitly extend stubbed class and overwrite method "publish"
        $publisherStub = new class () extends Publisher {
            public function publish($message)
            {
                assert($message === 'expexted message');
            }
        };
        $testedObject = new Foo;
        $testedObject->setPublisher($publisherStub->reveal());
        $testedObject->publish();
    }
}

As a side note: if your Foo class requires Publisher for its work you should set it via constructor, not setter method. Use setter methods only for optional dependencies


UPDATE

So from the comments I suggest in the actual code you are creating object of Publisher class with new like this

public function publishMessage() {
    $message   = $this->generateMessage();
    $publisher = new Publisher;
    $publisher->publish($message);
}

Or maybe you are using Redis::publish static method directly

public function publishMessage() {
    $message = $this->generateMessage();
    Redis::publish($message);
}

Well, this is called coupled classes and is considered a bad practice, because violates D in SOLID. Nonetheless, there is a workaround for mocking/stubbing dependency in such cases, again with anonymous classes.

Assuming that dependency class didn't loaded yet you can do something like this:

$class = new class() {
    function publish(string $message) {
        assert($message === 'expected');
    }
};
class_alias(get_class($class), 'Redis');

If you repeat this trick in multiple tests you will get warning:

PHP Warning: Cannot declare class Redis, because the name is already in use

To overcome it you will need to run your tests with --process-isolation

I think we should never do it (it is a dirty hack) and use DI, but sometimes we deal with legacy

like image 62
Nikita U. Avatar answered Nov 11 '22 02:11

Nikita U.