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

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!

If you found this article useful, please click [Like]!
7
Loading...
7 votes, average: 1.00 / 17
2,257
X Facebook Hatena Bookmark pocket

The person who wrote this article

About the author

Eita

First-year engineer
who wants to go outside