Skip to content
This repository has been archived by the owner on Oct 25, 2022. It is now read-only.

jaceju-tutorial-examples/phpunit-authentication

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lab2 - 測試身份驗證邏輯

目標:學習如何在程式碼中做依賴注入,以便做隔離測試。

無法被測試的程式碼

打開 src/AuthenticationService.php ,瞭解運作方式。

打開 tests/AuthenticationServiceTest.php ,查看驗證方式。

// $this->assertTrue($actual);// 註解拿掉。

執行測試。

採用「提煉方法並覆寫」的測試方式

編輯 AuthenticationService 類別,將 isValid 方法中的:

$profile = new Profile();
$customPassword = $profile->getPassword($account);

用 Extract Method 與 Inline 重構為:

$customPassword = $this->getCustomPassword($account);

與:

protected function getCustomPassword($account)
{
    $profile = new Profile();
    return $profile->getPassword($account);
}

將:

$rsaToken = new RsaToken();
$randomCode = $rsaToken->getRandom($account);

用 Extract Method 與 Inline 重構為:

$randomCode = $this->getRandomCode($account);

及:

protected function getRandomCode($account)
{
    $rsaToken = new RsaToken();
    return $rsaToken->getRandom($account);
}

編輯 AuthenticationServiceTest 測試類別,在檔案後加上:

class StubAuthenticationService extends AuthenticationService
{
    public function getCustomPassword($account)
    {
        return 'abc';
    }

    public function getRandomCode($account)
    {
        return '000000';
    }
}

修改 `` 測試方法,將:

$target = new AuthenticationService();

改為:

$target = new StubAuthenticationService();

執行測試。

手動建立 Stub

先還原到一開始的範例。

src/ 建立 IProfile.php ,內容為:

<?php

namespace Lab2;

interface IProfile
{
    public function getPassword($account);
}

src/ 建立 IRsaToken.php ,內容為:

<?php

namespace Lab2;

interface IRsaToken
{
    public function getRandom($account);
}

AuthenticationService 類別裡加入以下程式碼:

    /**
     * @var IProfile
     */
    private $profile;

    /**
     * @var IRsaToken
     */
    private $rsaToken;

    public function __construct(IProfile $profile, IRsaToken $rsaToken)
    {
        $this->profile = $profile;
        $this->rsaToken = $rsaToken;
    }

isValid 方法裡的:

$profile = new Profile();
$customPassword = $profile->getPassword($account);

取代成:

$customPassword = $this->profile->getPassword($account);

以及:

$rsaToken = new RsaToken();
$randomCode = $rsaToken->getRandom($account);

取代成:

$randomCode = $this->rsaToken->getRandom($account);

打開 tests/AuthenticationServiceTest.php ,在檔案後方加入:

class StubProfile implements IProfile
{
    public function getPassword($account)
    {
        return "abc";
    }
}

class StubRsaToken implements IRsaToken
{
    public function getRandom($account)
    {
        return "000000";
    }
}

並記得引用正確的類別:

use Lab2\IProfile;
use Lab2\IRsaToken;

it_should_be_valid() 方法中的:

$target = new AuthenticationService();

改成:

$profile = new StubProfile;
$rsaToken = new StubRsaToken;
$target = new AuthenticationService($profile, $rsaToken);

執行測試。

用 Mockery 建立 Stub

tests/AuthenticationServiceTest.php 檔案中引用 Mockery 類別:

use Mockery as m;

AuthenticationServiceTest 類別中加入 tearDown() 方法,並呼叫 m::close 來清除所有 Mockery 所建立的 mock 物件。

public function tearDown()
{
    m::close();
}

透過 m::mock(className) 來動態產生 stub 物件,也就是將:

$profile = new StubProfile;
$rsaToken = new StubRsaToken;

改為:

$profile = m::mock(IProfile::class); // ::class 為 PHP 5.5 的新語法
$rsaToken = m::mock(IRsaToken::class);

設定當呼叫 stub 物件的「哪個方法」,「傳入什麼參數」時,要「回傳什麼」?在 // Act 註解前直接加入以下程式碼:

$profile->shouldReceive('getPassword') // 要呼叫的方法
    ->withAnyArgs()                    // 傳入任何參數
    ->andReturn('abc');                // 固定的回傳值

$rsaToken->shouldReceive('getRandom')
    ->withAnyArgs()
    ->andReturn('000000');

// Act

刪掉原來的 StubProfileStubRsaToken 類別定義。

執行測試。

加入 Log 機制

將原來 AuthenticationService 類別的建構式:

    public function __construct(IProfile $profile, IRsaToken $rsaToken)
    {
        $this->profile = $profile;
        $this->rsaToken = $rsaToken;
    }

加入 log 物件參考,也就是改成:

    /**
     * @var ILog
     */
    private $log;

    public function __construct(IProfile $profile, IRsaToken $rsaToken, ILog $log)
    {
        $this->profile = $profile;
        $this->rsaToken = $rsaToken;
        $this->log = $log;
    }

isValid 方法的回傳式:

return $password === $validPassword;

加入是否紀錄 log 判斷,也就是改成:

$isValid = $password === $validPassword;

if (!$isValid) {
    // @todo 如何驗證當有非法登入的情況發生時,有正確記錄下來?
    $content = sprintf('Account: %s try to login failed', $account);
    $this->log->log($content);
}

return $isValid;

src/ 建立 ILog.php ,內容為:

<?php

namespace Lab2;

interface ILog
{
    public function log($content);
}

執行測試。

思考看看在還沒有實作 ILog 介面的類別前,要怎麼驗證 ILog::log 方法有被呼叫?

用 Mockery 建立 Mock

tests/AuthenticationServiceTest.phpit_should_be_valid 方法中加入 log 物件,也就是把:

$target = new AuthenticationService($profile, $rsaToken);

改成:

$log = m::mock(ILog::class);
$target = new AuthenticationService($profile, $rsaToken, $log);

要記得引用 Lab2\ILog 類別。

執行測試。

AuthenticationServiceTest 加入以下新方法 it_should_be_not_valid

/**
 * @test
 */
public function it_should_be_not_valid()
{
}

it_should_be_valid 方法的內容複製到 it_should_be_not_valid 方法裡。

用 mock 來測試驗證不通過時,應該會寫入 log 的情境。在 // Act 註解前加入以下程式碼:

$log->shouldReceive('log')
    ->once()
    ->with('Account: john try to login failed')
    ->andReturnNull();

執行測試。

想想會什麼測試會失敗?

將驗證的部份從:

// Act
$actual = $target->isValid('john', 'abc000000');

// Assert
$this->assertTrue($actual);

改成:

// Act
$actual = $target->isValid('john', 'abc999999');

// Assert
$this->assertFalse($actual);

執行測試。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages