As we all know, writing tests is very useful when developing applications. But sometimes you might find issues when writing tests that are really hard to detect. If you ever got “Serialization of ‘Closure’ is not allowed” / “unserialize(): Error at offset 0 of 186 bytes in” errors when running PhpUnit together with Mockery – you might be interested how to solve them, if you haven’t yet – you might be interested how to avoid them in future to save your time.
Let’s create simple route:
1 |
Route::get('/service','ServiceController@test'); |
and simple service class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?php namespace App\Services; class SumService { /** * Calculates sum * * @return int */ public function sum() { return $this->calculate() + 5; } /** * Make internal calculations * * @return int */ protected function calculate() { return 2 ** 3; } } |
and really basic controller:
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php namespace App\Http\Controllers; use App\Services\SumService; class ServiceController extends Controller { public function test(SumService $service) { return $service->sum(); } } |
Now, let’s create very simple test for our controller:
1 2 3 4 5 6 7 8 9 10 |
<?php class ExampleTest extends TestCase { public function testBasicExample() { $this->get('/service'); $this->assertEquals(13,$this->response->getContent()); } } |
when we run in command line phpunit
we should get green. No problem here.
Okay, but now, let’s assume that our service is something much more complicated, and we would like to mock it.
If we add the following method:
1 2 3 4 5 6 7 |
public function testWithMockSum() { $mock = Mockery::mock('overload:App\Services\SumService')->makePartial(); $mock->shouldReceive('sum')->once()->andReturn(15); $this->get('/service'); $this->assertEquals(15,$this->response->getContent()); } |
and run phpunit only for this method with the following command:
1 |
phpunit --filter=testWithMockSum |
we will again get green.
However when we run tests for both methods running phpunit
we will get error:
Could not load mock App\Services\SumService, class already exists
Again, no real problem here. We need to add extra PhpUnit comments to run each test in separate process like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php /** * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class ExampleTest extends TestCase { public function testBasicExample() { $this->get('/service'); $this->assertEquals(13,$this->response->getContent()); } public function testWithMockSum() { $mock = Mockery::mock('overload:App\Services\SumService')->makePartial(); $mock->shouldReceive('sum')->once()->andReturn(15); $this->get('/service'); $this->assertEquals(15,$this->response->getContent()); } } |
and now we we run phpunit
(for both tests) we will get green again. Ok, still no big problem.
Okay, we mocked sum
method. Let’s mock now only calculate
method. So again we add new method:
1 2 3 4 5 6 7 8 |
public function testWithMockCalculate() { $mock = Mockery::mock('overload:App\Services\SumService')->makePartial() ->shouldAllowMockingProtectedMethods(); $mock->shouldReceive('calculate')->once()->andReturn(15); $this->get('/service'); $this->assertEquals(20, $this->response->getContent()); } |
and now we run again phpunit
and as result we are getting now:
PHPUnit_Framework_Exception: [Exception]
Serialization of ‘Closure’ is not allowedCaused by
ErrorException: unserialize(): Error at offset 0 of 186 bytes in ../vendor/phpunit/phpunit/src/Util/PHP.php:114
Luckily, when we look into laravel log, we will find cause of the error:
Method App\Services\SumService::sum() does not exist on this mock object
So it seems, that Mockery overload
cannot make it as partial. Fortunately we can fix this quite easily. If we change our method into:
1 2 3 4 5 6 7 8 9 |
public function testWithMockCalculate() { $mock = Mockery::mock('App\Services\SumService')->makePartial() ->shouldAllowMockingProtectedMethods(); $mock->shouldReceive('calculate')->once()->andReturn(15); App::instance('App\Services\SumService', $mock); $this->get('/service'); $this->assertEquals(20, $this->response->getContent()); } |
we will get green again.
Okay, now let’s jump into the most tricky part. Let’s assume that it might happen that our controller can return also other statuses. Let’s imagine we used some route model binding or we used some middleware that will return 404 status. In our case let’s simulate this adding simple abort
into our controller like so:
1 2 3 4 5 |
public function test(SumService $service) { abort(404); return $service->sum(); } |
now, let’s run only the last test using:
1 |
phpunit --filter=testWithMockCalculate |
what are we getting now? We should get again:
PHPUnit_Framework_Exception: [Exception]
Serialization of ‘Closure’ is not allowedCaused by
ErrorException: unserialize(): Error at offset 0 of 186 bytes in ../vendor/phpunit/phpunit/src/Util/PHP.php:114
Okay, you think no problem – let’s look again into laravel log file. Well, the problem is that you won’t find anything useful there! You should get only something like this:
testing.ERROR: Exception: Serialization of ‘Closure’ is not allowed in -:68
Stack trace:
#0 -(68): serialize(Array)
#1 -(100): __phpunit_run_isolated_test()
#2 {main}
so it says nothing. Is it possible to track the problem? Well, if you don’t know what to look, it might be really hard. In our sample code it’s really obvious because we know what we changed in our controller and we have only a few lines of code, but what in case we use multiple services (probably with multiple mocks)? Well, in our case we could do something like this: let’s remove the comment (only temporary) from our test class we added to run each test in separate process:
1 2 3 4 |
/** * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ |
and now run again only this single test (you won’t be able to run all tests as you removed this comment).
Now you should get quite standard PhpUnit failed assertion that the big HTML is not equal to expected 20.
Why the hell when having comment enabled we are getting completely useless information “Serialization of ‘Closure’ is not allowed”? To be honest – I have no idea but looking on Internet I was not the only one who had this issue. I was trying to update my 4.8.* PhpUnit to 5.4.* but the error was exactly the same. The most strange thing was that on my PC it was working really fine whereas on devserver it failed.
In that case my devserver was reaching API rate limiting so it was getting 429 response instead of expected 200 and that’s why it received this error. So the problem was exactly the same as we simulated in our test – we were expecting 200 response with some data, but we got 404 response (we added it manually in our controller just to cause the error) and the whole test was failing with very unclear message and no useful log.
So next time when you use PhpUnit separate processes and get this error I hope you will know what you should search and you will know that probably something is not working as you expect and the error you get can be fixed.
Pat
your solution didn’t work, i will get the same
Mockery\Exception\RuntimeException: Could not load mock \MyClass class already exists
Marcin Nabiałek
Are you sure you have exact comments as I showed?