[Must-see for beginners] From the basics of Mock to how to use it *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 to return a different value depending on the number of times it is called.
- 2.3 3.I want to set a value to the class property of a mocked object
- 2.4 4. I want to mock some methods of the class to be tested
- 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 work!
It's been half a year since I joined the workforce, and I'm scared of the speed of time in many ways (the pressure of my second year as a working adult, the education of next year's new graduates, and the impatience of the difference between the passage of time and the speed of technological growth). etc.)
Ma! Putting my anxiety and frustration aside, this time I will talk about mocks when testing with PHP.
I would like to explain "What is a mock?", "Actual usage situations, and various ways to use it"!
Nice to meet you! *This is a method using Mockery.
What is a mock?
First of all, what is a mock? I will explain from there.
Mock-up (model) is an abbreviation for mock-up, which replaces the classes that the test target depends on when performing unit tests verifies whether the classes and methods are called correctly. It's for!
Also, with one of the test doubles, you can specify the return value of the method + verify the call arguments and number of times
However, if this is all you have, you might be wondering what it is all about, so let me give you a specific example of how to use it...
Case 1
We implemented a method for making payments using an external API.
So, when you decide to run the test, if you don't use mocks, you will have to access an external API every time you run the test, incurring charges , and write a description that executes a large number of methods. If you do so, a situation such as `` a large amount of access to the server that provides the external API will cause trouble (unintentional DoS attack) '' may occur.
Case 2 When the prerequisite method is not implemented
Suppose you want to test a method whose behavior changes depending on the bool value that is the return value of the method.
However, we have not yet completed implementing the method that returns a bool value.
What should I do! Well, you could just implement it first, but let's assume for some reason you can't implement it first.
in! Mocks are useful for situations like the above two!
Actual usage (with code)
Basic usage
This time, as an example, let's say we want to test a method that obtains payment information and performs payment using an external payment service (API).
At that time, you need to mock the part that uses the external payment service (API).
I briefly wrote the code for mocking in a series of steps.
The part that uses mocks starts from line 38
// A class that uses an external payment service class PaymentDataFromExternal { // A method that returns true if the payment is completed successfully public function paymentImmediately(int $paymentId) { //Returns true if the payment is successful return true; } } class PaymentService { public function __construct( private PaymentDataFromExternal $paymentDataFromExternal ){} // Method to be tested public function execPayment() { $payment = fetchPaymet(); // Get payment information // Execute payment from the obtained payment information (external Perform API communication) $isPayment = $this->paymentDataFromExternal->paymentImmediately($payment->id); return $isPayment; } } class Test { public function test_payment_Is the payment executed successfully?() { // Temporary Create payment information for $payment = Payment::factory(); // Class where the method you want to mock exists $this->mock(PaymentDataFromExternal::class, function($mock) { $mock->shouldReceive('paymentImmediately ') // Method name ->with($payment->id) // Argument ->andReturn(true); // Return value }); // Replace the class to be mocked with a mock object $paymentService = app( PaymentService::class); // Method execution replaced by mock object $result = $paymentService->execPayment(); $this->assertTrue($result); } }
So, I would like to list some of the mock patterns that I actually encountered (many of which are not directly described in the mockery official 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 (return value is Collection) that retrieves all users from the users table.
However, if you just pass the User model as the return value when mocking, you will get angry that the return value is different, and you will not be able to mock it properly.
After looking into it, it seems that mocking requires that the argument and return types match the actual method you want to mock
Therefore, in this case, it was necessary to make the specified class inherit using anonymous class
The actual code is below.
Example: Mock a method that retrieves all user information using a collection type 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 make it the specified type and successfully mock it.
2. When a mocked method is called multiple times, I want 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 to be tested, and I would like to mock it.
If you want the same return value both times, you can write it like andReturn(true), and it will return true every time the mocked method is called, so there is no problem, but in this case, the first time is true, I want it to return false the second time .
In this case, if you specify something like andReturn(1st time, 2nd time)
Example: If you want to return true the first time and false the second time with a method that determines whether it can be used.
$mock->shouldReceive('isEnabled') ->twice() ->andReturn(true, false);
3.I want to set a value to the class property of a mocked object
This time, I want to mock the method that retrieves user information from the users table.
but,
Since a value is set to a class property in a method, I want to verify that the value is set correctly.
Of course, I couldn't set a value to a class property just by mocking it, which was a problem.
In this case, you can set the value to the class property by using andSet(property, value you want to set)
Example: Mock the method that returns user information and set the name property to "Mr. Sato"
$mock->shouldReceive('getUserInfo') ->once() ->andReturn($user) ->andSet('name', 'Mr. Sato');
4. I want to mock some methods of the class to be tested
This time, there is a class that needs to be mocked in the same class as the method to be tested.
If you simply mock the method, other than the method being tested will also be replaced with the mock object.
Therefore, only the method to be tested must be mocked.
In this case, " partial mock " seems to be effective.
However, when I just mocked it using partial mock, it was not replaced with the mock object properly.
When I looked into it...
It seems that a partial mock cannot be replaced with a mock object just by instantiating it, and it seems that it is necessary to explicitly inject dependencies on this side.
class SampleClass { public function getName() { etc... } public function fetchDataFromExternal() { // Description that performs 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 mentioned above, you can use partial mocks by using makePartial, and you can also create dependencies by writing something like $this->instance(class name::class, partial mock object) Looks like it can be injected.
5. I want to mock static methods
This time, I want to mock the static method that creates the payment history.
However, normal mocking modifies the behavior of an object by mocking instance methods, and static methods use a special method because they are tied to the class itself rather than the instantiated class instance. It seems you need to.
To solve this problem, there is something called alias mock
Example: Mock static method that creates payment history based on payment ID and price
Mockery::mock('alias:'.History::class)->shouldReeive('makePurchase')->with($payment->id, $payment->price)->andReturn($purchaseHistory);
It seems that you can mock a static method by writing it like ('alias:'.class name::class) as shown above
However...
Although mocking static methods is supported using the above method, the official documentation states that it is not recommended
The reason is that an error will occur if a class with the same name exists in two or more tests, so it seems necessary to separate each test.
Solution: You need to write the following in Doc (however, this still causes many other problems, so it is still not recommended).
You can find it by searching the description below.
/** * @runInSeparateProcess * @preserveGlobalState disabled */
Reference source for the above description: kaihttps://docs.phpunit.de/en/11.4/annotations.html
That's all for today's introduction.
The explanations of the methods used above, including once(), with(), andReturn() (there are many others), are described in the official documentation, so please refer to it.
Mockery documentation: https://docs.mockery.io/en/latest/
summary
What do you think? Did it help you a little? ?
I am writing articles about mocking myself, and every time I come across a new pattern that I want to mock, I think to myself, ``How do I do this?!'' and I try to do it every day (maybe...).
So, this time we talked about mocks in test doubles, but test doubles also include stubs, spies, fake objects, and dummy objects.
I'm only aware of the simple behavior myself, so if you're interested, I'd appreciate it if you could look into it and let me know lol.
I'll be posting a lot of information from a budding engineer's perspective in the future, so be sure to add it to your favorites!