4 months ago, I was assigned a task to write Unit tests for the project, this is also the first time I was exposed to PHPUnit. While writing tests encountered many problems that make it difficult to write tests, this article I would like to share one of the problems I encountered offline.
My project is Digmee project, because I cannot share the code out, I will use the most basic examples to describe.
One of the problems I encountered and took a lot of time for it is: How to test one function is the parameter of another function?
I have found out and these are called Anonymous functions, so …
What are anonymous functions?
In PHP, there is a concept that is used quite a lot, especially with modern frameworks like Laravel, which is a function parameter of another function, also known as Anonymous functions, also commonly known as Closure.
For example:
1 2 3 4 | Route::get('welcome', function() { return 'Welcome'; }); |
The get function above has 2 parameters, the first parameter is 1 string and the second parameter is an anonymous functions. The problem is how to test those anonymous functions?
To be able to test, we will use: …
Mockery
We will use this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class PostRepository { public function getPostsByUser(int $userId) { } } class PostService { private $postRepository; public function __construct(PostRepository $postRepository) { $this->postRepository = $postRepository; } public function getPostsByUser(int $userId) { return $this->postRepository->getPostsByUser($userId); } } |
Now we will test PostService
in Unit test
, unlike Integration test
must actually call the dependency method (PostRepository), must connect different types of database or must run through many functions of many classes. Unit tests simply make sure the method of the dependency is called and must be called correctly.
The test code will look like this:
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 27 28 29 30 31 | class PostServiceTest extends TestCase { /** @var PostRepository|MockInterface */ private $postRepository; /** @var PostService */ private $postService; protected function setUp() { parent::setUp(); $this->postRepository = Mockery::mock(PostRepository::class); $this->postService = new PostService($this->postRepository); } public function testGetPostsByUser() { $posts = [ 'post 1', 'post 2', ]; $userId = 1; $this->postRepository->shouldReceive('getPostsByUser') ->with($userId) ->once() ->andReturn($posts); $this->assertEquals($posts, $this->postService->getPostsByUser($userId)); } } |
Here we use Mockery to simulate a mocked object. In the above example that we wish mockery by the method testGetPostsByUser
of class PostService
ensure the following:
- Must call the
getPostsByUser
method ofPostRepository
- Must call exactly 1 time
- Must call with exactly one parameter of
$userId
- Assuming the function of
PostRepository
returns any abcxyz, then the function ofPostService
must return exactly that.
Later, if someone in the team changes the function code in PostService
, wants to pass another parameter, or wants to call the function 69 times instead of just calling once … then I expect that test must be failed to also know the way that counts. Either go (tat) for a play because you dare change the behavior of the function or begged people to accidentally make people become pregnant, you have to go and be responsible.
So … What would Mockery associate with Anonymous functions?
Mock anonymos function
Modifying the code a bit, PostService
will use the whole CacheService
to retrieve post data from the cache. The getFromCache
function will have 2 parameters, the first parameter is cacheKey
, the second parameter is an Anonymous functions
to retrieve data, if cacheService
not find data by cacheKey
, it will call this function to retrieve data to save to the cache then return it.
The new code will be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class PostService { private $postRepository; /** @var CacheInterface */ private $cacheService; public function __construct(PostRepository $postRepository) { $this->postRepository = $postRepository; } public function getPostsByUser(int $userId) { $cacheKey = 'user-posts-' . $userId; return $this->cacheService->getFromCache($cacheKey, function () use ($userId) { return $this->postRepository->getPostsByUser($userId); }); } } |
My problem in Digmee project is this, I don’t know how to test this so-called Anonymous functions …
And I found Mockery that supports how to validate paramater with Mockery :: on and the parameter is also an anonymous functions.
The test code will look like this:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class PostServiceTest extends TestCase { /** @var PostRepository|MockInterface */ private $postRepository; /** @var CacheInterface|MockInterface */ private $cacheService; /** @var PostService */ private $postService; protected function setUp() { parent::setUp(); $this->postRepository = Mockery::mock(PostRepository::class); $this->cacheService = Mockery::mock(CacheInterface::class); $this->postService = new PostService($this->postRepository); } public function testGetPostsByUser() { $posts = [ 'post 1', 'post 2', ]; $userId = 1; $this->postRepository->shouldReceive('getPostsByUser') ->with($userId) ->once() ->andReturn($posts); $this->cacheService->shouldReceive('getFromCache') ->with( 'user-posts-' . $userId, Mockery::on(function ($closure) use ($posts){ return $closure() == $posts; }) ); $this->assertEquals($posts, $this->postService->getPostsByUser($userId)); } } |
Here we have mocked [email protected]
to return a sample data of posts, and this function is called again in the Anonymous functions of CacheService
, so we will verify by calling this Anonymous functions, the result will be equal to the data. Whether the post has mocked above.
I refer to this link .
1 2 3 4 | Mockery::on(function ($closure) use ($posts){ return $closure() == $posts; }) |
And finally, the assert data returned by [email protected]
also returned the exact results as we wanted.
1 2 | $this->assertEquals($posts, $this->postService->getPostsByUser($userId)); |
Auspicious
Above is my practical experience encountered when writing tests, and I have found a lot to be able to solve it ( for example ).
There are many other issues but I’m a bit lazy to share I hope you raise your bow (bow).
Thank you for reading!