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.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With