首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 設計模式:偽對象模式在單元測試中的應用 PHP 設計模式:偽對象模式在單元測試中的應用

     編輯:PHP 綜合知識

偽對象模式:測試難以構建依賴時的利器

偽對象 在單元測試中模擬真實物件行為:提供預設回應、追蹤方法呼叫、驗證呼叫順序。SimpleTest、PHPUnit、Mockery 等框架皆內建支援。

核心概念

類型 用途 特點
Dummy 佔位參數 不被實際使用
Stub 提供預設回應 不驗證行為
Spy 記錄呼叫資訊 事後驗證
Mock 預設期望 + 驗證 行為驗證核心
Fake 簡化實作 如記憶體資料庫

PHPUnit 內建 Mock 實作

<?php
use PHPUnit\Framework\TestCase;

// 被測類別:依賴外部 API
class PaymentService {
    public function __construct(private PaymentGateway $gateway) {}
    
    public function charge(float $amount, string $currency): bool {
        return $this->gateway->charge($amount, $currency);
    }
}

// 介面
interface PaymentGateway {
    public function charge(float $amount, string $currency): bool;
}

// 測試類別
class PaymentServiceTest extends TestCase {
    public function testChargeSuccess() {
        // 1. 建立 Mock
        $mockGateway = $this->createMock(PaymentGateway::class);
        
        // 2. 設定期望:呼叫 charge(100, 'USD') 回傳 true
        $mockGateway->expects($this->once())
                    ->method('charge')
                    ->with($this->equalTo(100.0), $this->equalTo('USD'))
                    ->willReturn(true);
        
        // 3. 注入測試對象
        $service = new PaymentService($mockGateway);
        
        // 4. 執行並驗證
        $result = $service->charge(100, 'USD');
        $this->assertTrue($result);
    }
    
    public function testChargeFailure() {
        $mockGateway = $this->createMock(PaymentGateway::class);
        $mockGateway->expects($this->once())
                    ->method('charge')
                    ->willReturn(false);
        
        $service = new PaymentService($mockGateway);
        $this->assertFalse($service->charge(50, 'EUR'));
    }
    
    public function testChargeException() {
        $mockGateway = $this->createMock(PaymentGateway::class);
        $mockGateway->expects($this->once())
                    ->method('charge')
                    ->willThrowException(new GatewayException('網路錯誤'));
        
        $service = new PaymentService($mockGateway);
        $this->expectException(GatewayException::class);
        $service->charge(100, 'USD');
    }
}
?>

Mockery:更流暢的 API (需安裝)

<?php
// composer require mockery/mockery
use Mockery as m;

class PaymentServiceMockeryTest extends TestCase {
    public function tearDown(): void {
        m::close(); // 清理容器
    }
    
    public function testChargeWithMockery() {
        $gateway = m::mock(PaymentGateway::class);
        $gateway->shouldReceive('charge')
                ->once()
                ->with(100, 'USD')
                ->andReturn(true);
        
        $service = new PaymentService($gateway);
        $this->assertTrue($service->charge(100, 'USD'));
    }
    
    public function testMultipleCalls() {
        $gateway = m::mock(PaymentGateway::class);
        $gateway->shouldReceive('charge')
                ->times(3) // 或 atLeast()->once()->atMost()->times(5)
                ->withArgs([100, 'USD']) // 部分驗證
                ->andReturn(true, false, true); // 連續回傳不同值
        
        $service = new PaymentService($gateway);
        $this->assertTrue($service->charge(100, 'USD'));
        $this->assertFalse($service->charge(100, 'USD'));
        $this->assertTrue($service->charge(100, 'USD'));
    }
}
?>

進階:部分 Mock (Spy 真實物件部分方法)

<?php
class UserService {
    public function __construct(private UserRepository $repo) {}
    
    public function getUserName(int $id): string {
        $user = $this->repo->find($id);
        return $user ? $user->name : 'Guest';
    }
    
    public function getUserCount(): int {
        return $this->repo->count();
    }
}

// 只 Mock find(),保留 count() 真實實作
class UserServiceTest extends TestCase {
    public function testPartialMock() {
        $realRepo = new SqlUserRepository($pdo); // 真實實作
        $mockRepo = $this->createPartialMock(UserRepository::class, ['find']);
        
        $mockRepo->expects($this->once())
                 ->method('find')
                 ->with(1)
                 ->willReturn(new User(1, 'Alice'));
        
        // count() 會呼叫真實實作 (需資料庫)
        // 通常搭配資料庫測試或 SQLite 記憶體資料庫
    }
}
?>

測試資料庫交互:Fake vs Mock

<?php
// Fake: 記憶體實作 (快速、無外部依賴)
class InMemoryUserRepository implements UserRepository {
    private array $users = [];
    
    public function save(User $user): void {
        $this->users[$user->id] = $user;
    }
    
    public function find(int $id): ?User {
        return $this->users[$id] ?? null;
    }
    
    public function count(): int {
        return count($this->users);
    }
}

// 整合測試使用 Fake
class UserServiceIntegrationTest extends TestCase {
    public function testWithFakeRepo() {
        $repo = new InMemoryUserRepository();
        $repo->save(new User(1, 'Alice'));
        $repo->save(new User(2, 'Bob'));
        
        $service = new UserService($repo);
        $this->assertEquals('Alice', $service->getUserName(1));
        $this->assertEquals(2, $service->getUserCount());
    }
}
?>

偽對象最佳實踐與反模式

✅ 最佳實踐 ❌ 反模式
Mock 介面而非具體類別 Mock 具體類別 (綁定實作細節)
只 Mock 必要方法 (避免過度規範) 所有方法都設定期望
驗證行為 (方法呼叫) 而非狀態 斷言內部屬性值
使用 with() 精確驗證參數 不驗證參數或用 withAnyArgs()
測試名稱描述行為: testChargeReturnsTrueWhenGatewaySucceeds 測試名稱模糊: testCharge
外部依賴 (DB、API、檔案) 必須 Mock/Fake 單元測試連真實資料庫

避免 Mock 過度:關注契約測試

<?php
// 契約測試:驗證實作符合介面契約
interface PaymentGatewayContract {
    public function charge(float $amount, string $currency): bool;
}

// 所有實作必須通過此測試
abstract class PaymentGatewayContractTest extends TestCase {
    abstract protected function createGateway(): PaymentGatewayContract;
    
    public function testChargeReturnsBool() {
        $gateway = $this->createGateway();
        $result = $gateway->charge(100, 'USD');
        $this->assertIsBool($result);
    }
    
    public function testChargeWithZeroAmount() {
        $gateway = $this->createGateway();
        $this->expectException(InvalidArgumentException::class);
        $gateway->charge(0, 'USD');
    }
}

// Stripe 實作測試
class StripeGatewayContractTest extends PaymentGatewayContractTest {
    protected function createGateway(): PaymentGatewayContract {
        return new StripeGateway('sk_test_...');
    }
}

// Mock 測試只需驗證「正確呼叫介面」,不重複驗證介面契約
?>
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved