Taken from the Mockery documentation: "In unit tests, mock objects simulate the behaviour of real objects. They are commonly utilised to offer test isolation, to stand in for objects which do not yet exist, or to allow for the exploratory design of class APIs without requiring actual implementation up front."
Mocking allows us to replace certain functions or complete objects, with special objects or functions that allow us to control their behavior in a very basic way.
In simple terms the "mock object" will extend the class and override one or more of its functions. The constructor can also be overridden or disabled if necesssary.
After a new object is created, we would need to specify the new behavior of some functions. In most cases we would have define an expectation and the result that should be produced when that expectation is met. We will get into more details about that later on.
Sometimes we also need to mock some of PHP's internal functions. Let's say that for example our code uses the file_get_contents()
function to read a file (in production this file is always available and produced by another system). We cannot guarantee that the file will always be available on any given file system, so it would be very convienent if in the special case where we are executing our test cases we can temporarily control the behavior of the functions (to for example let it return the content that we need).
When we use the "Interface Segregation" principle of the SOLID principles, it is very much possible that our class depends on an interface rather than a class. There are cases thinkable where there are not any objects that implement the interface yet. In that case we can use a mock object based on the interface and determine the behavior of the implemented functions by defining the expectations.
As we discussed in the chapter about unit testing, we should only test our units (as they are our responsibility). However in many cases our business logic will have one or more dependencies on other classes (not part of our unit). These other classes should have their own unit tests and should not be tested by the test cases for the class that we actually want to test.
One of the reasons that we don't want to test the dependecies with the test cases for our class, is that it will become increasingly difficult to identify where potential problems occur. Imagine that the dependency is broken, not only will the test cases for the dependency fail, but the test cases for the class that has the dependencies will also fail (or if lots of classes have a dependency on that broken class, lots of test cases will fail). If there are lots of tests that fail, it is difficult to determine what is causing the problem.
Another reason is that by proxy our logic might communicate with an external service like a database for example, generally this slows down the execution of the actual code that we want to test.
Yet another reason could be that some things just are not available or cannot be used in testing. These things could include a collection of files in the file system, an API that is very expensive, the API having to return a special value (can only be returned in rare cases), etc...
What we can do in those cases is use Mocking.
How exactly depends on the framework that is used. Mockery is a very popular mocking framework, but it is also possible to use the facilities of PHPUnit. Since Mockery is a lot more flexible it is the recommended choice.
Let's say that the class that we want to test looks something like:
class MassMailer
{
/** @var MailerFactory */
private $mailerFactory;
/** @var MassMailRecipients */
private $recipients;
public function __construct(
MailerFactory $mailerFactory,
MassMailRecipients $recipients
){
$this->mailerFactory = $mailerFactory;
$this->recipients = $recipients;
}
private function sendMail(string $address): bool
{
$mailer = $this->mailerFactory->getMailer();
$mailer->setSubject('Mass mail');
$mailer->setBody(
'This is a mass email, if you no longer want to receive these'
. ' emails then you are out of luck because we do not provide'
. ' a working unsubscribe form...'
);
$mailer->setAddress($address);
return $mailer->send();
}
public function sendMails(): bool
{
$addresses = $this->recipients->getEmailAddresses();
foreach($addresses as $address) {
if(!$this->sendMail($address)) {
return false;
}
}
return true;
}
}
As we can see our class or SUT (System Under Test), depends on the classes MailerFactory
and MassMailRecipients
. We should only test the code of this class and not the code of the dependencies. Also we don't want to actually send any mails (testing that the email servers work and are set up correctly, goes way beyond the scope of unit testing).
Another thing is that, in order for us to be able to test the negative test cases (at least 1 email could not be sent), we have to be able to control the output of the mailers that are produced by the MailerFactory
.
Without mocking our test case class with the positive test case ("happy case") will look something like:
<?php
namespace Scuti\Mail;
use PHPUnit\Framework\TestCase;
class MassMailerTest extends TestCase
{
public function testCanInstantiate()
{
$massMailer = new MassMailer(new MailerFactory, new MassMailRecipients);
$this->assertInstanceOf(MassMailer::class, $massMailer);
return $massMailer;
}
/**
* @depends testCanInstantiate
*/
public function testCanSendMails(MassMailer $massMailer)
{
$this->assertTrue($massMailer->sendMails());
}
}
Considering the application is probably configured in such a way that it can send emails, executing these test cases will use the dependencies and send emails to all the addresses specified by the MassMailRecipients
class. That class may in turn get the addresses from a file on the file system or from a database. Both of those methods will slow down the execution of the test suite. The mailers provided by MailerFactory
will send the emails using the network (also slowing down test suite execution).
For those reasons it would be much better to mock the dependencies:
<?php
namespace Scuti\Mail;
use PHPUnit\Framework\TestCase;
use Mockery;
class MassMailerTest extends TestCase
{
public function testCanInstantiate()
{
$mailerFactoryMock = Mockery::mock('Scuti\Mail\MailerFactory');
$massMailRecipientsMock = Mockery::mock('Scuti\Mail\MassMailRecipients');
$massMailer = new MassMailer($mailerFactoryMock, $massMailRecipientsMock);
$this->assertInstanceOf(MassMailer::class, $massMailer);
return $massMailer;
}
/**
* @depends testCanInstantiate
*/
public function testCanSendMails(MassMailer $massMailer)
{
$this->assertTrue($massMailer->sendMails());
}
}
Now we have declared our mock objects, however this will fail the second test case. Our mock objects cannot know how to behave and by default if no expectations are specified an exception will be thrown. So let's start with the expectations of the MassMailRecipients
mock. What we can see is that there should be a call to getEmailAddresses()
and something that can be iterated should be returned (let's say that it should be an array). We can achieve that by adding some lines, making the test case class look like:
<?php
namespace Scuti\Mail;
use PHPUnit\Framework\TestCase;
use Mockery;
class MassMailerTest extends TestCase
{
public function testCanInstantiate()
{
$mailerFactoryMock = Mockery::mock('Scuti\Mail\MailerFactory');
$massMailRecipientsMock = Mockery::mock('Scuti\Mail\MassMailRecipients');
$massMailRecipientsMock->shouldReceive('getEmailAddresses')
->andReturn([
'info@example.com',
'test@example.com'
]);
$massMailer = new MassMailer($mailerFactoryMock, $massMailRecipientsMock);
$this->assertInstanceOf(MassMailer::class, $massMailer);
return $massMailer;
}
/**
* @depends testCanInstantiate
*/
public function testCanSendMails(MassMailer $massMailer)
{
$this->assertTrue($massMailer->sendMails());
}
}
Now we have to declare the expectations for the MailerFactory
, this is however a factory, so it will do not much more than initializing an object and returning it. This tells us that there is actually another dependency that is less visible as the others, but needs to be mocked nonetheless. So how can we declare expectations in such a way that another type of mock is returned?
<?php
namespace Scuti\Mail;
use PHPUnit\Framework\TestCase;
use Mockery;
class MassMailerTest extends TestCase
{
public function testCanInstantiate()
{
$mailerMock = Mockery::mock('Scuti\Mail\Mailer');
$mailerMock->shouldReceive('setSubject');
$mailerMock->shouldReceive('setBody');
$mailerMock->shouldReceive('setAddress');
$mailerMock->shouldReceive('send')
->andReturn(true);
$mailerFactoryMock = Mockery::mock('Scuti\Mail\MailerFactory');
$mailerFactoryMock->shouldReceive('getMailer')
->andReturn($mailerMock);
$massMailRecipientsMock = Mockery::mock('Scuti\Mail\MassMailRecipients');
$massMailRecipientsMock->shouldReceive('getEmailAddresses')
->andReturn([
'info@example.com',
'test@example.com'
]);
$massMailer = new MassMailer($mailerFactoryMock, $massMailRecipientsMock);
$this->assertInstanceOf(MassMailer::class, $massMailer);
return $massMailer;
}
/**
* @depends testCanInstantiate
*/
public function testCanSendMails(MassMailer $massMailer)
{
$this->assertTrue($massMailer->sendMails());
}
}
As we can see in the example above, we just specify that the getMailer()
function will return the new mock object $mailerMock
. We have now successfully mocked all the dependencies of this class and our test cases verify that we can instantiate the class. We also verify that in the case where sending all mails is successful our MassMailer
also gives a positive response.
Let's add a negative test case, where we verify that if one of the mailers fails to send the email our response is also negative. For this to work we need to have the call to send()
on the mailer mock return false
. That means that we cannot use the same initialization code for the mocks as we are using in testCanInstantiate()
, we can however still depend on the test:
<?php
namespace Scuti\Mail;
use PHPUnit\Framework\TestCase;
use Mockery;
class MassMailerTest extends TestCase
{
private function getMailerFactoryMock($mailerSendReturns = true)
{
$mailerMock = $this->getMailerMock($mailerSendReturns);
$mailerFactoryMock = Mockery::mock('Scuti\Mail\MailerFactory');
$mailerFactoryMock->shouldReceive('getMailer')
->andReturn($mailerMock);
return $mailerFactoryMock;
}
private function getMailerMock($sendReturns = true)
{
$mailerMock = Mockery::mock('Scuti\Mail\Mailer');
$mailerMock->shouldReceive('setSubject');
$mailerMock->shouldReceive('setBody');
$mailerMock->shouldReceive('setAddress');
$mailerMock->shouldReceive('send')
->andReturn($sendReturns);
return $mailerMock;
}
private function getMailRecipientsMock()
{
$massMailRecipientsMock = Mockery::mock('Scuti\Mail\MassMailRecipients');
$massMailRecipientsMock->shouldReceive('getEmailAddresses')
->andReturn([
'info@example.com',
'test@example.com'
]);
return $massMailRecipientsMock;
}
public function testCanInstantiate()
{
$mailerFactoryMock = $this->getMailerFactoryMock(true);
$massMailRecipientsMock = $this->getMailRecipientsMock();
$massMailer = new MassMailer($mailerFactoryMock, $massMailRecipientsMock);
$this->assertInstanceOf(MassMailer::class, $massMailer);
return $massMailer;
}
/**
* @depends testCanInstantiate
*/
public function testCanSendMails(MassMailer $massMailer)
{
$this->assertTrue($massMailer->sendMails());
}
/**
* @depends testCanInstantiate
*/
public function testSendMailsReturnsFalseWhenSendingFails()
{
$mailerFactoryMock = $this->getMailerFactoryMock(false);
$massMailRecipientsMock = $this->getMailRecipientsMock();
$massMailer = new MassMailer($mailerFactoryMock, $massMailRecipientsMock);
$this->assertFalse($massMailer->sendMails());
}
}
What we can see here is that all the code that initializes the mock objects is now separated into separate functions. Two of these functions now have a parameter that allows us to control the return value of the mailer object. This way we can control what code path is executed and assert that the result is what we expect it to be.
The expectations we declared in our example above are very basic. Also at this point they are not verified. For example in the test case testCanInstantiate
we pass the MassMailer two mock objects that both have expectations declared with the shouldReceive()
function. The functions specified by the expectations "should" be called, but since they are called from the sendMails()
function (which is not called in that test case) they will in fact never be called. It is possible to declare how many times the function is expected to be called and by default it is set to "any" number of times. Roughly 0 or more times.
However if we would set the expectation of the function getEmailAddresses()
to "once" instead of "any", we specify that we expect that the function of this mock object will be called exactly 1 time. It is very useful to be able to check things like that, but we also have to be very careful that our test does not become too tightly coupled with the implementation of the code.
Our test cases should be basic input/output tests, where we verify the output based on a certain input. If we start verifying that our code calls every function that it needs to on the mock objects and that it calls those functions the right amount of times, the chances are pretty big that our test case is becoming too tightly coupled with the implementation, however there are also cases in which you might need to do just that.
In any case if you need to verify that the expectations on the mock objects are fulfilled, you can do that by adding a fixture like this:
public function tearDown()
{
Mockery::close();
}
By calling close()
on the Mockery
class the expectations will be verified and the test will fail if one of the expectations is not fulfilled. By overriding the tearDown()
function this will be called after every test case in this test case class.
This article just describes some basic concepts and provides some basic examples Of course much more is possible using mocking, have a look at the documentation of Mockery: http://docs.mockery.io/en/latest/reference