【初心者必見】Mockの基本から使い方まで※PHP,Mockery

皆様お疲れ様です!

社会人1年目の駆け出しエンジニアのえいたです!

社会人になり早半年、時の流れの速さにいろんな意味で怯えております(迫りくる社会人2年目のプレッシャー、来年の新卒の教育、時間経過と技術的成長速度の差での焦りetc..)

ま!そんな私の不安や焦燥感は置いといて今回はPHPでテストを行う際の mock について

「モックとはなんぞや」、「実際の使う場面、様々な使い方」について解説できたらと思います!

よろしくです!※Mockeryを使用した方法です。

モック(mock)とは

はじめにモックってなんだい?ってところから説明いたします。

mock-up(模型)の略語で、実装したコードの動きをユニット(単体)テストする際に、テスト対象が依存しているクラスを置き換えて、そのクラス及びメソッドが正しく呼ばれているかを検証するためのものです!

また、テストダブルのうちの1つで、メソッドの返り値の指定 + 呼び出し引数・回数の検証を行うことができます。

ですがこれだけだとだから何だよって感じなので、使用する際の具体例を挙げますと、、、

ケース1

外部のAPIを使用し決済を行うメソッドの実装を行いました。

ですので、よしテストを回してみようって時に、モックを使用していないと、テストを実行するたびに外部APIにアクセスし「料金が発生」、また、該当メソッドが大量に実行される記述をしていた場合には「外部APIを提供しているサーバーに大量のアクセスを行うことで迷惑がかかる(無意識にDos攻撃)」などの事態がおこります。

ケース2前提となるメソッドが実装されていない場合

あるメソッドの返り値であるbool値によって挙動が変わるメソッドをテストするとします。

しかし、まだbool値を返すメソッドの実装が完了していません。

どうしようか!まあ、先に実装してやればよいのですが、何らかの理由で先に実装できないと仮定します。

で!上記2つのような事態にモックが役立つのです!

実際の使い方(コードあり)

基本的な使い方

今回は、例として決済情報を取得し外部決済サービス(API)を利用し決済を行っているメソッドをテストしたいとします。

その際に、外部決済サービス(API)を利用している部分をモック化する必要があります。

モック化する際のコードをざっと一連の流れで書いてみました。

モックを使用している部分は、38行目からになります。


    // 外部決済サービスを使用するクラス
    class PaymentDataFromExternal {
    
       // 決済が正常に終了した場合trueを返すメソッド
       public function paymentImmediately(int $paymentId) {
    
          //決済実行し成功したらtrueを返す
          return true;
       }
    }
    
    class PaymentService {
    
        public function __construct(
            private PaymentDataFromExternal $paymentDataFromExternal
        ){}
    
        // テスト対象になるメソッド
        public function execPayment() {
    
            $payment = fetchPaymet(); // 決済情報を取得
    
            // 取得した決済情報から決済実行(外部API通信を行う)
            $isPayment = $this->paymentDataFromExternal->paymentImmediately($payment->id);
    
            return $isPayment;
       }
    }
    
    class Test {
    
        public function test_payment_決済が正常に実行されいるか() {
    
            // 仮のpayment情報を作成
            $payment = Payment::factory();
    
            // モック化したいメソッドが存在するクラス
            $this->mock(PaymentDataFromExternal::class, function($mock) {
                $mock->shouldReceive('paymentImmediately') // メソッド名
                     ->with($payment->id) // 引数
                     ->andReturn(true); // 返り値
            });
    
            // モック化対象のクラスをモックオブジェクトに置き換える
            $paymentService = app(PaymentService::class);
    
            // モックオブジェクトに置き換えられたメソッド実行
            $result = $paymentService->execPayment();
    
            $this->assertTrue($result);
        }
    }
    

で、実際に私が遭遇したモックを一部のパターン(mockery公式ドキュメントに直接的に記述のないものを多めに)を上げたいと思います。

1つ注意なのですが、テスト時にモックを利用するには、必ず適切にDI(依存性注入)がなされている必要があるようです。

1.モック化したメソッドの返り値を何かしらのクラスを継承したオブジェクトで返したい

全ユーザーをusersテーブルから取得するメソッド(返り値がCollection)をモックしたいです。

しかし、モック化する際の返り値にUserモデルを渡すだけでは返り値の方が違うよと怒られ上手くモック化できません。

調べてみるとモックは引数と返り値の型がモックしたい実際のメソッドと一致する必要があるようです。

ですので、この場合はPHPの機能である無名クラスを使って指定のクラスを継承させる必要がありました。

実際のコードが下記になります。

例:user_idをもとにコレクション型で全ユーザー情報を取得するメソッドをモック

    $mock->shouldReceive('fetchUsersById')
         ->once()
         ->with($user->id)
         ->andReturn(new class extends Illuminate\Database\Eloquent\Collection([$userMock]));
    

上記のように、「new class extends 継承したいクラス」と記述を行うことで指定の型にでき、無事モック化することが出来ました。

2.モック化したメソッドが複数回呼ばれる際、呼ばれた回数によって別の値を返したい

今回は、テスト対象のメソッドの中で2度呼ばれるメソッドがあり、こちらをモックしたいです。

2度とも同じ返り値でよい場合は、andReturn(true)のように書いておくと、モック化したメソッドが呼ばれるたびに true を返してくれるので問題はないですが、今回は1度目はtrue、2度目はfalseを返してほしいです。

この場合には、「andReturn(1度目,2度目)」のように指定してやると呼ばれた回数によって違う返り値を返してくれます。

例:使用可能か判定するメソッドで一回目はtrue二回目はfalseを返したい場合

 
    $mock->shouldReceive('isEnabled')
         ->twice()
         ->andReturn(true, false);
    

3.モック化したオブジェクトのクラスプロパティに値をセットしたい

今回は、ユーザー情報をusersテーブルから取得するメソッドをモック化したいです。

しかし、

メソッドの中でクラスプロパティに値をセットしているため、きちんと値がセットされていることを検証したい

ただモック化するだけではもちろんクラスプロパティに値をセットすることはできず、困りました。

このような場合は、「andSet(プロパティ, セットしたい値)」を使うことでクラスプロパティに値をセットできます。

例:ユーザー情報を返すメソッドをモックしnameプロパティを「佐藤さん」にセットする

       $mock->shouldReceive('getUserInfo')
            ->once()
            ->andReturn($user)
            ->andSet('name', '佐藤さん');
    

4.テスト対象のクラスの一部メソッドをモックしたい

今回はテスト対象のメソッドと同じクラスにモック化が必要なクラスが存在しています。

単純にモック化してしまうと、テスト対象のメソッド以外もモックオブジェクトに置き換えられてしまいます。

そのため、テスト対象のメソッドのみモック化しなければなりません。

この場合は「パーシャルモック」が有効なようです。

ですが、パーシャルモックを使用してモック化しただけだと、うまくモックオブジェクトに置き換わってくれませんでした。

調べてみると、、、

パーシャルモックは、インスタンス化するだけではモックオブジェクトに置き換えることは出来ないようで、こちら側で明示的に依存性を注入する必要があるようです。

    class SampleClass {
        public function getName() {
            etc...
        }
        public function fetchDataFromExternal() {
            // 外部通信を行う記述 
        }
    }
    class テスト {
        $partialMock = Mockery::mock(SampleClass::class)->makePartial();
        $mock->shouldReceive('fetchDataFromExternal')
             ->once();
        // 下記を記述することでクラスが呼ばれた際に、$partialMockに置き換えられる
        $this->instance(SampleClass::class, $partialMock);
    }
    

上記のように、makePartialを使用することでパーシャルモックを使用することができ、また、「$this->instance(クラス名::class, パーシャルモックオブジェクト)」のように記述することで、依存性注入できるようです。

5.静的メソッドをモック化したい

今回は決済履歴を作成するstatic メソッドをモック化したいです。

しかし、通常のモックは、インスタンスメソッドをモック化してオブジェクトの振舞いを変更するものであり、静的メソッドはインスタンス化されたクラスのインスタンスではなく、クラス自体に紐づいているため特殊な方法を使用する必要があるようです。

そして、これを解決するために「エイリアスモック」というものが用意されています。

例:決済IDと値段をもとに決済履歴を作成するstaticメソッドをモック

    Mockery::mock('alias:'.History::class)->shouldReeive('makePurchase')->with($payment->id, $payment->price)->andReturn($purchaseHistory);
    

上記のように「('alias:'.クラス名::class)」のように記述することで静的メソッドをモック化できるようです。

ですが、、、

一応上記の方法で静的メソッドのモックはサポートされているのですが、公式ドキュメントに推奨されないと記載があります。

理由は、2つ以上のテストで同じ名前のクラスが存在するとエラーが発生するため、テスト毎に分離する必要があるようです。

解決策:Docに下記の記述をしてやる必要がある(しかし、これでもその他問題は多数あるため、やはり非推奨です)。

下記の記述で調べていただくと出てきます。

    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    

上記記載参照元:kaihttps://docs.phpunit.de/en/11.4/annotations.html

今回の紹介は以上になります。

上記で使用している、once(),with(),andReturn()も含め(他にもいろいろある)、メソッドの説明は公式ドキュメントに記述してありますので、ぜひそちらを参照してください。

Mockeryドキュメント:https://docs.mockery.io/en/latest/

まとめ

いかがでしたでしょうか、少しはお役に立てたでしょうか??

私自身モックの記事を書いているわけですが、モック化したい新しいパターンに遭遇するたびに「どうやるんだよ!!」と思いながら日々何とかやっております(たぶん...)。

で、今回はテストダブルの中のモックを取り上げたわけですが、テストダブルには他にも、スタブ、スパイ、フェイクオブジェクト、ダミーオブジェクトがあります。

私自身簡単な挙動しか認知していないため、興味があればぜひ調べて教えて頂けたら幸いです笑。

駆け出しのエンジニア目線での情報をこの先いくつも投稿するためぜひお気に入りにでも入れておいてください!

この記事がお役に立てば【 いいね 】のご協力をお願いいたします!
7
読み込み中...
7 票, 平均: 1.00 / 17
2,080
X facebook はてなブックマーク pocket

この記事をかいた人

About the author

えいた

1年目のエンジニア
とりあえず外に出たい