[Must-see for beginners] Mock basics and usage guide ※PHP, Mockery

table of contents
- 1 What is a mock?
- 2 Actual usage (with code)
- 2.1 1. I want to return the return value of a mocked method as an object that inherits some class
- 2.2 2. When a mocked method is called multiple times, I want it to return a different value depending on the number of times it is called
- 2.3 3. I want to set a value for a class property of a mocked object
- 2.4 4. I want to mock some methods of the class under test
- 2.5 5. I want to mock static methods
- 3 summary
Thank you everyone for your hard work!
I'm Eita, a budding engineer in my first year of working life!
It's been six months since I started working, and I'm feeling scared in many ways by how quickly time passes (the pressure of my second year, the training of new graduates next year, the frustration of the difference in the speed of time and technological growth, etc.)
Well, I'll leave my anxiety and impatience aside and talk about mocking when testing in PHP.
I would like to explain what a mock is, the actual situations in which it is used, and various ways to use it!
Thank you! *This is a method using Mockery
What is a mock?
First, let me explain what a mock is
unit testing the operation of implemented code , it replaces the classes that the test subject depends on and verifies that the classes and methods are being called correctly !
Additionally, one of the test doubles can be used to specify the return value of a method, as well as verify the call arguments and number of times
But if that's all there is to it, it might seem a bit pointless, so here's a concrete example of how to use it..
Case 1
We have implemented a method to make payments using an external API
So, if you decide to run a test without using a mock, the external API will be accessed every time you run the test, resulting in a charge . Also, if the method is written in such a way that it is executed a large number of times, it could cause inconvenience to the server that provides the external API by making large amounts of access (an unintentional DoS attack) .

Case 2: When the required method is not implemented
Suppose you want to test a method whose behavior changes depending on the Boolean value returned by the method
However, we have not yet completed the implementation of the method that returns a boolean value
What should I do? Well, I could just implement it first, but let's assume that for some reason I can't implement it first
So! Mocks are useful in the above two situations!
Actual usage (with code)
Basic usage
As an example, we will now test a method that obtains payment information and uses an external payment service (API) to make the payment
In this case, you will need to mock the parts that use external payment services (APIs)
I have written a rough outline of the code required for mocking
The part where the mock is used starts from line 38
// Class that uses an external payment service class PaymentDataFromExternal { // Method that returns true if payment is completed successfully public function paymentImmediately(int $paymentId) { // Executes payment and returns true if successful return true; } } class PaymentService { public function __construct( private PaymentDataFromExternal $paymentDataFromExternal ){} // Method to be tested public function execPayment() { $payment = fetchPaymet(); // Obtains payment information // Executes payment from the obtained payment information (communicates with external API) $isPayment = $this->paymentDataFromExternal->paymentImmediately($payment->id); return $isPayment; } } class Test { public function test_payment_was payment executed successfully?() { // Create temporary payment information $payment = Payment::factory(); // Class containing the method to be mocked $this->mock(PaymentDataFromExternal::class, function($mock) { $mock->shouldReceive('paymentImmediately') // Method name ->with($payment->id) // Arguments ->andReturn(true); // Return value }); // Replace the mocked class with a mock object $paymentService = app(PaymentService::class); // Execute the method replaced with the mock object $result = $paymentService->execPayment(); $this->assertTrue($result); } }
So, I would like to list some of the mocking patterns I have actually encountered (most of which are not directly described in the official mockery documentation)
One thing to note is that in order to use mocks during testing, it seems that DI (dependency injection) must be done properly
1. I want to return the return value of a mocked method as an object that inherits some class
I want to mock a method (returning a Collection) that retrieves all users from the users table
However, if you simply pass the User model as the return value when mocking, you will get an error saying that the return value is different and you will not be able to mock it properly
After some research, it seems that mocks need to have argument and return types that match the actual method you want to mock
Therefore, in this case, it was necessary to use the PHP feature of anonymous classes to inherit the specified class.
The actual code is as follows:
Example: Mock a method to get all user information as a collection based on user_id
$mock->shouldReceive('fetchUsersById') ->once() ->with($user->id) ->andReturn(new class extends Illuminate\Database\Eloquent\Collection([$userMock]));
As shown above, by writing " new class extends the class you want to inherit ", you can specify the type and successfully mock it.
2. When a mocked method is called multiple times, I want it to return a different value depending on the number of times it is called
This time, there is a method that is called twice among the methods being tested, and I would like to mock this method
If it's okay for the return value to be the same both times, you can write it as andReturn(true), which will return true every time the mocked method is called, so there's no problem, but in this case I want it to return true the first time and false the second time .
In this case, if you specify andReturn(1st, 2nd)
Example: If you want to return true the first time and false the second time in a method that determines whether something is available
$mock->shouldReceive('isEnabled') ->twice() ->andReturn(true, false);
3. I want to set a value for a class property of a mocked object
This time, we want to mock the method that retrieves user information from the users table
but,
I want to verify that the value is set correctly because I am setting a value to a class property within a method
I was in trouble because I couldn't just mock it and set values to the class properties
In such cases, you can set a value to a class property by using andSet(property, value to set)
Example: Mock a method that returns user information and set the name property to "Sato-san"
$mock->shouldReceive('getUserInfo') ->once() ->andReturn($user) ->andSet('name', 'Mr. Sato');
4. I want to mock some methods of the class under test
In this case, there is a class that needs to be mocked in the same class as the method being tested
If you simply mock it, methods other than the one being tested will also be replaced with mock objects
Therefore, you should only mock the methods you are testing
In this case, " partial mocking " seems to be effective.
However, if you simply mock using a partial mock, it will not be properly replaced with a mock object
Upon investigation, I found that..
It seems that partial mocks cannot be replaced with mock objects simply by instantiating them; it seems that we need to explicitly inject dependencies on this side
class SampleClass { public function getName() { etc... } public function fetchDataFromExternal() { // Code to perform external communication } } class Test { $partialMock = Mockery::mock(SampleClass::class)->makePartial(); $mock->shouldReceive('fetchDataFromExternal') ->once(); // By writing the following, when the class is called, it will be replaced with $partialMock $this->instance(SampleClass::class, $partialMock); }
As shown above, you can use partial mocks by using makePartial, and it seems that dependency injection is possible by writing $this->instance(class name::class, partial mock object)
5. I want to mock static methods
This time, I want to mock the static method that creates payment history
However, normal mocking involves mocking instance methods to change the behavior of an object, and static methods are tied to the class itself, not to the instance of the instantiated class, so it seems necessary to use a special method
To solve this problem, there is something called an alias mock
Example: Mock a static method that creates a payment history based on the payment ID and price
Mockery::mock('alias:'.History::class)->shouldReeive('makePurchase')->with($payment->id, $payment->price)->andReturn($purchaseHistory);
It seems that you can mock static methods by writing ('alias:'.classname::class) as shown above
However..
Although the above method does support mocking static methods, the official documentation states that it is not recommended
The reason is that if two or more tests have a class with the same name, an error occurs, so it seems necessary to separate each test
Solution: You need to add the following to the doc (but this still has many other issues so is not recommended):
You can find it by checking the description below
/** * @runInSeparateProcess * @preserveGlobalState disabled */
Reference source for the above statement: kaihttps://docs.phpunit.de/en/11.4/annotations.html
That's all for this introduction
The official documentation provides explanations of the methods used above, including once(), with(), and Return() (there are many others), so please refer to it
Mockery documentation: https://docs.mockery.io/en/latest/
summary
What do you think? Was it of some help to you?
I myself write articles about mockups, and every time I come across a new pattern that I want to mock, I think to myself, "How am I going to do that?!", but I manage to do my best every day (probably...)
So, this time we looked at mocks, which are a type of test double, but there are other types of test doubles: stubs, spies, fake objects, and dummy objects
I'm only aware of simple behaviors, so if you're interested, I'd be grateful if you could research it and let me know
I'll be posting lots of information from the perspective of a budding engineer in the future, so be sure to add me to your favorites!
7