Updated with shift from sniffs to Rector rules, that handle these cases much better.
Martin Hlaváč had a very nice talk about testing in Berlin PHP Meetup last week (while I hosted with Rector), and one of the topic was mocking.
I often see developers fighting with this, in places they don't have to, just because this topic is so widespread all over the internet and unit tools.
Did you know there is easier and more clear way to do "mocking"?
At the time being, there is only 1 post about anonymous classes in tests (thanks to Matthieu!). Compared to that, there are many PHP tool made just for mocking: Prophecy, Mockery, PHPUnit native mocks, Mockista and so on. If you're a developer who uses one of them, knows that he needs to add proper annotations to make autocomplete work, has the PHPStom plugin that fixes bugs in this autocomplete and it works well for you, just stop reading.
This post is for developers who struggle with mocking and have a feeling, that they're doing something wrong.
You're not. It's the mocking part. Mocks are often the bottleneck of understanding in tests. They're so easy to make, that they can overpopulate your tests... the same way units test can test every getter and setter of all your entities in 20 minutes (hint: not a way to go).
willReturn()
, willReturnAny()
or willReturnExact()
?Let's get to code. Real open source code from one of my code-reviews that inspired me to make this post:
namespace PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$heurekaCategoryFacade = $this->createHeurekaCategoryFacadeMock();
// ...
}
/**
* @return \PHPUnit\Framework\MockObject\MockObject|\Shopsys\ProductFeed\HeurekaBundle\Model\HeurekaCategory\HeurekaCategoryFacade
*/
private function createHeurekaCategoryFacadeMock()
{
$returnCallback = function ($categoryId) {
if ($categoryId === self::CATEGORY_ID_FIRST) {
return $this->heurekaCategory;
}
return null;
};
/** @var HeurekaCategoryFacade|\PHPUnit\Framework\MockObject\MockObject $heurekaCategoryFacadeMock */
$heurekaCategoryFacadeMock = $this->createMock(HeurekaCategoryFacade::class);
$heurekaCategoryFacadeMock
->method('findByCategoryId')
->willReturnCallback($returnCallback);
return $heurekaCategoryFacadeMock;
}
}
The code is intentionally more complex, so we have real-life example, instead of made-up code with Car
class and open()
method that no-one can relate to.
Now answer me in 5 seconds:
Now try to implement your idea. If you made it under another 60 seconds and your tests pass, you master mocking well and there is nothing for you to learn from this post.
What happened to use in reality? We got stuck for at least 30 minutes on modification of methods like that. Studying PHPUnit manual and looking to StackOverflow with my favorite PHPUnit mock method multiple calls with different arguments.
That's not what tool should do for you. Tools should work for you, not you for them.
Let me show an alternative approach that has the same result.
~ 95 % developers can read this code, even if they see PHPUnit for the first time:
namespace PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$heurekaCategoryFacade = $this->createHeurekaCategoryFacade();
// ...
}
private function createHeurekaCategoryFacadeMock()
{
// anonymous class mock
return new class extends HeurekaCategoryFacade
{
public function findByCategoryId($categoryId)
{
if ($categoryId === self::CATEGORY_ID_FIRST) {
return $this->heurekaCategory;
}
return null;
}
};
}
}
We don't need no PHPStorm plugin, memorized methods from mock framework nor duplicated|annotations.
I believe now we all made it under 5 seconds with both answers:
namespace PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$heurekaCategoryFacade = $this->createHeurekaCategoryFacade();
// ...
}
private function createHeurekaCategoryFacade()
{
// anonymous class mock
return new class extends HeurekaCategoryFacade
{
public function findByCategoryId($categoryId)
{
- if ($categoryId === self::CATEGORY_ID_FIRST) {
+ if ($categoryId === self::CATEGORY_ID_FIRST || $categoryId === 7) {
return $this->heurekaCategory;
}
return null;
}
};
}
}
The code already tells us what to do next.
Some people mock because they follow good practice and make every class abstract or final. They don't want to deal with constructors, that would often lead to more mocking. It's great practice and super easy to put make classes final with Rector CI:
composer require rector/rector --dev
use Rector\SOLID\Rector\Class_\FinalizeClassesWithoutChildrenRector;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(FinalizeClassesWithoutChildrenRector::class);
};
Yes, it's that simple, you just saved your project from most of its legacy code. But final
classes should not be the reason to choose to mocks. Well, you can also hack the final
and use mocking right away, or you can go with the code flow. Dance with it!
You don't need to go on a mocking spree. The constructor issue naturally lead us to abstract an interface refactoring.
We create a new interface:
interface CategoryFacadeInterface
{
public function findByCategoryId($categoryId);
}
And use it in anonymous class:
private function createHeurekaCategoryFacade()
{
// anonymous class mock
- return new class extends HeurekaCategoryFacade
+ return new class implements CategoryFacadeInterface
{
public function findByCategoryId($categoryId)
{
// ...
}
};
}
And now you respect SOLID principles - your code is:
Also, your application can now use abstraction (= interface) instead of specific implementation (= class). That leads to autowiring benefits, decoupling from monolith and better service replace-ability.
They say your code is 10x more read than written on average. I believe it's at least 1000x in open-source. Knowing that we want our code, not to be just clear and readable, but to be
Let's close this with Occam's razor:
One should select the answer that makes the fewest assumptions
Pick a solution that is understandable to the most people. No tool, posts or studying tutorials or reading books is needed. People will thank you and your code will attract more people because they'll feel confident to manage the code. Then naturally, your code will get more contributions from happy developers. Win win :)
Happy anonymocking!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!