CodeIgniter4のフィーチャーテストでモックを使う

この記事は CodeIgniter Advent Calendar 2020 - Qiita の17日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

CodeIgniter4のフィーチャーテストでデータベースを使う の続きです。

前回はテスト用のデータベースを用意しました。しかし、データベースのテストは必要ないので、モックを使いたいということもあります。

今回はモデルをモックにしてみましょう。

News コントローラの現状

現在のコントローラのコードは以下のようになっています。

app/Controllers/News.php

<?php
namespace App\Controllers;

use App\Models\NewsModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
use Kenjis\CI4Twig\Twig;

class News extends Controller
{
    /**
     * @var Twig
     */
    private $twig;

    public function initController(
        RequestInterface $request,
        ResponseInterface $response,
        LoggerInterface $logger
    ) {
        parent::initController(
            $request,
            $response,
            $logger
        );

        $this->twig = new Twig();
    }

    public function index()
    {
        $model = new NewsModel();

        $data = [
            'news'  => $model->getNews(),
            'title' => 'News archive',
            'main'  => 'news/overview.html',
        ];
//        dd($data);

        return $this->twig->render('news_tmpl.html', $data);
    }

    public function view($slug = null)
    {
        $model = new NewsModel();

        $data['news'] = $model->getNews($slug);

        if (empty($data['news']))
        {
            throw new PageNotFoundException('Cannot find the news item: '. $slug);
        }

        $data['title'] = $data['news']['title'];
        $data['main'] = 'news/view.html';
//        dd($data);

        return $this->twig->render('news_tmpl.html', $data);
    }

    public function create()
    {
        $model = new NewsModel();

        if ($this->request->getMethod() === 'post' && $this->validate([
                'title' => 'required|min_length[3]|max_length[255]',
                'body'  => 'required',
            ]))
        {
            $model->save([
                 'title' => $this->request->getPost('title'),
                 'slug'  => url_title($this->request->getPost('title'), '-', TRUE),
                 'body'  => $this->request->getPost('body'),
            ]);

            $data['main'] = 'news/success.html';

            return $this->twig->render('news_tmpl.html', $data);

        }
        else
        {
            $data = [
                'title' => 'Create a news item',
                'main' => 'news/create.html',
            ];

            return $this->twig->render('news_tmpl.html', $data);
        }
    }
}

$model = new NewsModel(); となっているため、この $model をモックに差し替えることができません。

News コントローラの変更

News コントローラを以下のように変更します。

--- a/app/Controllers/News.php
+++ b/app/Controllers/News.php
@@ -11,6 +11,11 @@ use Kenjis\CI4Twig\Twig;

 class News extends Controller
 {
+    /**
+     * @var NewsModel
+     */
+    private $newsModel;
+
     /**
      * @var Twig
      */
@@ -27,15 +32,15 @@ class News extends Controller
             $logger
         );

+        $this->newsModel = model(NewsModel::class);
+
         $this->twig = new Twig();
     }

     public function index()
     {
-        $model = new NewsModel();
-
         $data = [
-            'news'  => $model->getNews(),
+            'news'  => $this->newsModel->getNews(),
             'title' => 'News archive',
             'main'  => 'news/overview.html',
         ];
@@ -46,9 +51,7 @@ class News extends Controller

     public function view($slug = null)
     {
-        $model = new NewsModel();
-
-        $data['news'] = $model->getNews($slug);
+        $data['news'] = $this->newsModel->getNews($slug);

         if (empty($data['news']))
         {
@@ -64,14 +67,12 @@ class News extends Controller

     public function create()
     {
-        $model = new NewsModel();
-
         if ($this->request->getMethod() === 'post' && $this->validate([
                 'title' => 'required|min_length[3]|max_length[255]',
                 'body'  => 'required',
             ]))
         {
-            $model->save([
+            $this->newsModel->save([
                  'title' => $this->request->getPost('title'),
                  'slug'  => url_title($this->request->getPost('title'), '-', TRUE),
                  'body'  => $this->request->getPost('body'),

new キーワードではなく、CodeIgniter4 の model() 関数を使ってモデルクラスをインスタンス化するように変更しました。

テストコードの追加

モデルをモックに差し替えるテストコードを追加します。 CodeIgniter 4.0.4 でのコードです。

--- a/tests/feature/NewsTest.php
+++ b/tests/feature/NewsTest.php
@@ -2,6 +2,8 @@

 namespace Tests\Feature;

+use App\Models\NewsModel;
+use CodeIgniter\Database\ModelFactory;
 use CodeIgniter\Test\FeatureTestCase;

 class NewsTest extends FeatureTestCase
@@ -21,4 +23,31 @@ class NewsTest extends FeatureTestCase
         $result->assertSee('<title>CodeIgniter Tutorial</title>');
         $result->assertSee('Elvis was sighted at the Podunk internet cafe.');
     }
+
+    public function testGetNewsWithMock()
+    {
+        $newsModel = $this->getMockBuilder(NewsModel::class)
+            ->disableOriginalConstructor()
+            ->onlyMethods(['getNews'])
+            ->getMock();
+        $newsModel->method('getNews')
+            ->willReturn(
+                [
+                    [
+                        'title' => 'Mocked title',
+                        'slug' => 'mocked-title',
+                        'body' => 'Mocked body. Bra, bra, bra...',
+                    ],
+                ]
+            );
+        ModelFactory::injectMock('NewsModel', $newsModel);
+
+        $result = $this->get('news');
+
+        $result->assertStatus(200);
+
+        $result->assertSee('<title>CodeIgniter Tutorial</title>');
+        $result->assertSee('Mocked body. Bra, bra, bra...');
+        $result->assertDontSee('Elvis was sighted at the Podunk internet cafe.');
+    }
 }

CodeIgniter4 の ModelFactory::injectMock() メソッドで作成したモデルを注入します。

データベースにはない本文 Mocked body. Bra, bra, bra... が結果に含まれること、データベースに含まれている Elvis was sighted at the Podunk internet cafe. が結果に含まれないことを確認します。

テストの実行

phpunit を実行します。

$ vendor/bin/phpunit tests/feature/NewsTest.php
PHPUnit 8.5.13 by Sebastian Bergmann and contributors.

Error:         XDEBUG_MODE=coverage or xdebug.mode=coverage has to be set

..                                                                  2 / 2 (100%)

Time: 480 ms, Memory: 10.00 MB

OK (2 tests, 7 assertions)

通りました。

CodeIgniter 4.0.5 以降

開発版の CodeIgniter4 では ModelFactory が廃止予定となり、CodeIgniter 4.0.5 ではテストコードは以下のように書けます。

--- a/tests/feature/NewsTest.php
+++ b/tests/feature/NewsTest.php
@@ -3,7 +3,7 @@
 namespace Tests\Feature;

 use App\Models\NewsModel;
-use CodeIgniter\Database\ModelFactory;
+use CodeIgniter\Config\Factories;
 use CodeIgniter\Test\FeatureTestCase;

 class NewsTest extends FeatureTestCase
@@ -40,7 +40,7 @@ class NewsTest extends FeatureTestCase
                     ],
                 ]
             );
-        ModelFactory::injectMock('NewsModel', $newsModel);
+        Factories::injectMock('models', NewsModel::class, $newsModel);

         $result = $this->get('news');

CodeIgniter4でテストDBをMySQLにする に続きます。

この記事は CodeIgniter Advent Calendar 2020 - Qiita の17日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

参考

Date: 2020/12/17

Tags: codeigniter, codeigniter4, testing, database