This is the last article from the series about building from scratch a streaming Memcached PHP client for ReactPHP ecosystem. The library is already released and published, you can find it on GitHub.
In the previous article, we have completely finished with the source code for async Memcached ReactPHP client. And now it’s time to start testing it. The client has a promise-base interface:
And how should we test this code? Should we create an event loop and
run() it in every test? Or we don’t need it at all? Let’s figure it out.
It is necessary to decide what we are going to test. From the consumer’s point of view, the client returns promises, that can be resolved or rejected. And actually, all client methods return promises. So, we need to test that in some conditions these promises are resolved and in others - are rejected. Also, we can additionally check resolved values and rejection reasons. As an example, let’s test that the client resolves a pending request promise with the response from the server.
Under the hood, the client uses a duplex stream for communication with a server, but we are going to test the client code in isolation, so this dependency will be mocked.
So, we start with an empty test class:
The first thing we need to do is to set up the client and its dependencies:
setUp() method creates a mock and instantiates an instance of the client. Here is the source code of the client constructor:
As you can see, it immediately calls
on() method on the stream to attach some event handlers to
close events. That’s why I’ve set an expectation for
on() method on the stream mock:
shouldReceive('on'). Before implementing tests we need to add one more thing - a trait:
This trait is necessary for PHPUnit to assert mocked expectations. By default PHPUnit doesn’t count Mockery assertions and if there are no
$this->assert* calls in the test class, PHPUnit will report:
This test did not perform any assertions. So we should include this integration trait in the test class. When all these preparations are done we can move on to writing tests.
Assert Promise Resolves
So, we start with a simple test. It will check that the client resolves a promise from the request with a response data from the server. For these purposes, we will use client’s
version() method (that returns Memcached server version), because it is very simple and has no arguments. The scenario is the following:
- We call
- We assert that the promise from
version()method was resolved with the value of
To set mock expectations we need to refresh in memory what happens under the hood when we call
version() method on the client. The
Client class has no such method, and for all Memcached commands it actually uses magic
In our test, we have mocked instance of
$this->stream. So, we start our test with setting up expectations for this mock:
The code above can be described like this:
When we call
version()on the client, it should call
write()method on the stream. Then we call
version()method, which returns a promise.
And here comes the main section of this article: how to test a promise. In this particular test, we need to check that the promise resolves with the data from the server. We assume, that the server has returned a string
12345 as a server version. To pass server responses to the client we can use
resolveRequests() method. It accepts an array of responses and uses them to resolve pending requests:
The last step is assertion. Tests run synchronously, so we need to wait for a promise to be resolved. Then we get the resolved value and assert it with an expectation. For waiting (or running promises in a synchronous way) there is a nice library clue/php-block-react from Christian Lück. This library can be used for running ReactPHP async components in a traditional synchronous way - exactly what we need. After installing this library we have an access to a set of functions from
Clue\React\Block namespace. One of them is
await(PromiseInterface $promise, LoopInterface $loop, $timeout = null)
It accepts a promise, an instance of the event loop, and a timeout to wait. When the promise is resolved this function returns a resolved value. If the promise rejects or timeout is out, this function throws an exception. In our case we don’t have an event loop, so let’s create one. It will be used in many tests, so I’m going to instantiate it in
Now, the whole test looks like this:
To prove that the test actually tests the promise let’s change
assertEquals() expectation and see what happens:
As being expected the test fails, that means that assertions work fine.
Now, we can extract a custom assertion from it, so the test will look more explicit for the reader:
We have extracted custom
assertPromiseResolvesWith() assertion. It tries to resolve a promise. If the promise is resolved it checks the resolved value with an expected one. If the promise is rejected the test fails with a nice clear message. By default, this assertion waits for 2 seconds, because without
Block\await() function is going to wait endlessly.
For example, if our promise rejects we will get a nice message explaining it:
Assert Promise Rejects
The next step is to test that promise rejects. For example, in our case with Memcached client when the connection is closed the client rejects all incoming requests.
And again we can use the same
Block\await() function to assert that promise rejects. When this happens
Block\await() throws an exception which was the promise rejection reason. To create an assertion we can add an empty
If an exception was thrown we consider it as a passed test, otherwise the promise was resolved and we consider the test as a failed one.
To prove that test actually works as we expect, let’s remove
close() call and simulate that server has returned some responses with
This logic can be also extracted to a custom assertion like this:
Also, this assertion can be improved for use cases when we want to check the reason why the promise was rejected. The reason is always an instance of the
Exception class. To check that promise was rejected with a required reason we can use PHPUnit
assertInstanceOf assertion. Let’s rewrite the previous test and now also check the rejection reason. The client when being closed rejects all incoming requests with an instance of
assertPromiseRejectsWith() assertion under the hood calls
assertPromiseRejects() to check that the promise was actually rejected. Then simply checks an instance of the rejection exception. To prove that this assertion works, let’s assert a wrong exception (
LogicException) and see what happens:
We get a nice explaining message, why the test has failed.
Using Mocks For Assertions
There is another approach for testing promises - using mocks instead of waiting. The main idea is the following:
- create a callable mock
- add this mock as a resolve/reject handler
- set an expectation to this mock
- assert that this callable was/was not called.
First of all, we need a callable mock. In PHP
Closure class is declared as
final, so we cannot mock it. Instead, we can create our own implementation:
It is a simple class with the only one method
__invoke(). Then we add two basic assertion methods for resolving and for rejection:
These methods create a mock for our
CallableStub and set an expectation on it. Then we can use these mocks and add them as promise handlers. When promise resolves/rejects these mocks will be executed. Let’s write a dummy test, just to check how these assertions work:
To test that promise resolves we set
assertCallableCalledOnce() expectation as a resolving handler and
assertCallableCalledNever as a rejection one. If the promise resolves, the first callback is called one, and the second callback is never executed. And when we run the test it works!
To prove that it works, let’s now reject the promise:
The test fails, but can you see the reason why? And here comes a huge disadvantage when using mocks - meaningless messages. The test says that:
__invoke method and that it should be called, but not a word about promises, and why the test actually has failed. When using mocks we cannot provide custom fail messages, that’s why I don’t like this approach for testing promises and prefer to use
Clue\React\Block functions to wait for a promise and then simply run some assertions.
Also, if you write functional tests, that require running the loop, you tests will become even more tricky. Now, you should run the loop, wait for things to happen, then stop the loop, and only then run the assertions. Something like this:
Testing asynchronous code sometimes can be tricky. In this article I’ve covered two approaches for testing promises:
- running an event loop and waiting for a promise
- using mocks with expectations for promise handlers
As for me I prefer the first one (using clue/php-block-react library) because it is much easier to use, the tests look readable and failure messages exactly tell the reason why the tests have failed.
Writing this article has inspired me to create my own testing library for ReactPHP promises. It contains
TestCase class which extends base PHPUnit
TestCase and provides a set of convenient assertions:
So, if you are going to test ReactPHP promises try
seregazhuk/react-promise-testing library and use nice and readable assertions.