iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
3
Modern Web

Laravel 6.0 初體驗!怎麼用最新的 laravel 架網站!系列 第 8

[Day 8] 再聊自動測試!怎麼為我們的新網頁加上單元測試

既然有了新網頁,當然要好好的測試一下囉!

今天我們來聊聊怎麼對新網頁進行測試!

規劃測試項目

這個網頁比起前面的 hello world 要稍微複雜一些,所以我們的測試也會稍微複雜。

首先,在整合測試的部分,我們希望整體的功能最後呈現的是:

  • 如果連線到 inspire,HTTP status 是 200

各個元件的部分,因為之前的功能並沒有細分成不同元件實作,所以沒有討論。

我們針對 InspireController,應該要測試:

  • 呼叫 inspire 函式時,應該要呼叫對應的 InspireService->inspire(),並回傳其輸出

我們針對 InspireService,應該要測試:

  • 呼叫 inspire 函式時,應該要能回傳一句名言

大概這樣。

整合測試

整合測試前面已經做過很多次了,相信大家都很熟悉。我們這次測試檔案命名為 tests/Feature/InspireTest.php

(記得怎麼產生這個檔案嗎?忘記的話請回去看看猝不及防的自動測試教學!怎麼用 Laravel 撰寫自動測試

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class InspireTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testInspire()
    {
        $response = $this->get('/inspire');
        $response->assertStatus(200);
    }
}

通過之後,我們來規劃一下單元測試。

單元測試

測試 InspireController

現在我們要分別針對 InspireControllerInspireService 進行測試了。不過這個時候,我們遇到了一個問題⋯⋯

InspireController->inspire() 的內容是

public function inspire()
{
    return (new InspiringService())->inspire();
}

直接這樣測試的話,無論如何都會用到 InspiringService 呀!這該怎麼辦?

怎麼讓 Controller 和 Service 鬆綁?聊依賴注入

遇到這個問題,根本原因是因為,我們的 InspireController,其服務依賴於底層架構,也就是 InspiringService 的實作。

這樣的設計很常見,也很符合開發的思路。

不過,這樣的設計,相對來說缺乏彈性。舉現在的例子,當你想要測試時,就被這層依賴關係限制住,無法抽換了。

所以,我們要反轉這層依賴關係,使用依賴注入的方式進行實作。

首先,我們在 InspireController  裡面,加入一個 constructor:

public function __construct()
{

}

這是 PHP 裡面宣告建構子(constructor)的方式,每次建立 InspireController 時,都會先呼叫這個函式,來完成 InspireController 的初始化。

接著,我們這樣改寫:

<?php

namespace App\Http\Controllers;

use App\Services\InspiringService;
use Illuminate\Http\Request;

class InspiringController extends Controller
{
    private $service;
    public function __construct(InspiringService $inspiringService)
    {
        $this->service = $inspiringService;
    }

    /**
     * @return string
     */
    public function inspire()
    {
        return $this->service->inspire();
    }
}

這樣改寫有什麼好處?

用這樣的寫法,InspiringController 可以不使用 inspiringService 來完成其任務,而是任何繼承 inspiringService 的物件都可以!換句話說,如果我們想要修改其邏輯,我們只要撰寫某個類別,繼承 inspiringService 就好了。

這樣的寫法,增加了很大的彈性,在我們這次的狀況中,這份彈性正好可以用來完成單元測試的需求。

我們來建立 InspiringControllerTest 這個單元測試:

$ php artisan make:test --unit InspiringControllerTest

成功之後,我們就會在 tests/Unit 裡面看到 InspiringControllerTest.php

然後就是比較複雜的部分了,我們利用 Laravel 內建的 Mockery 來協助我們製作仿造物件(mock):

$mock = \Mockery::mock(InspiringService::class);

這樣撰寫,$mock 這個物件就成功的作為 InspiringService 的 mock。

接著,我們希望這個 mock 不僅僅只是仿造而已,他還能協助我們檢查InspiringController 是否成功呼叫了他的 inspire() 並回傳其內容。

所以,我們這樣做:

$mock->shouldReceive('inspire')->andReturn('名言');

很直觀吧!這樣宣告之後,$mock 會預期被呼叫 inspire,然後回傳「名言」

這樣設計環境之後,針對 InspiringController 的測試就很好撰寫了。我們可以這樣做:

$inspiringController = new InspiringController($mock);

宣告 InspiringController 時,不是使用 InspiringService,而是繼承 InspiringService$mock 物件

然後我們斷言 InspiringController->inspire() 的回傳,應該會等同 $mock 的回傳:

self::assertEquals(
    '名言',
    $inspiringController->inspire()
);

綜合起來就是

<?php

namespace Tests\Unit;

use App\Http\Controllers\InspiringController;
use App\Services\InspiringService;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class InspiringControllerTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function testInspire()
    {
        $mock = \Mockery::mock(InspiringService::class);
        $mock->shouldReceive('inspire')->andReturn('名言');
        $inspiringController = new InspiringController($mock);
        self::assertEquals(
            '名言',
            $inspiringController->inspire()
        );
    }
}

這樣,我們的測試就完成了。

測試 InspiringService

相比之下,InspiringService 就好測試多了,目前我們只需要確定他的 inspire() 確實會回傳字串而已:

動作如下:

  • 建立 tests/Unit/InspiringServiceTest.php
  • 建立 testInspire(),呼叫 InspiringService->inspire(),並確定回傳的型態是字串

內容如下:

<?php

namespace Tests\Unit;

use App\Services\InspiringService;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class InspiringServiceTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function testExample()
    {
        self::assertIsString(
            (new InspiringService())->inspire()
        );
    }
}

很簡單吧!


今天的文章結束囉!總結一下我們學到了什麼。

今天我們知道了 Laravel 裡面怎麼新增單元測試,知道怎麼用依賴注入的方式反轉原先的依賴關係。還學會了怎麼使用 Mockery!

今天的內容有點多,希望大家吸收起來還順利,我們明天見!


上一篇
[Day 7] 需要用到 Controller 了!淺聊一下網頁 MVC 框架的概念
下一篇
[Day 9] 建立資料庫!Laravel 怎麼做資料庫遷移
系列文
Laravel 6.0 初體驗!怎麼用最新的 laravel 架網站!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
jkl99
iT邦新手 5 級 ‧ 2019-09-11 00:20:30

Inspire 與 Inspiring 錯亂了/images/emoticon/emoticon06.gif

ReccaChao iT邦新手 1 級 ‧ 2019-09-11 01:41:08 檢舉

寫到一半時我後來全部都改成 inspire 比較簡單
途中改來改去真的很不好意思 ><

0
ckp6250
iT邦好手 1 級 ‧ 2019-11-27 11:11:18

太深奧了,有些難懂,
先跳過去,日後再回頭來看。

0
a600masool
iT邦新手 5 級 ‧ 2020-05-13 17:13:04

https://i.imgur.com/uXzu0RN.png
照著上面提供的方法做會出現紅色毛毛蟲
這要如何修正呢

這個是因為編輯器內建的語法矯正器發現你提供的 class 與 controller 需要的參數並不一致造成的

  1. 不使用強型態宣告 controller 依賴
  2. 用另外的方式產生 InspireController
$this->mock(InspireService::class);
$inspireController = $this->app->make(InspireController::class);
0
aa4731073
iT邦新手 4 級 ‧ 2022-06-30 12:29:19

請問有測試結果的截圖嗎?

我要留言

立即登入留言