使用 Laravel 中的自定义存根简化工作

发布于:2025-06-08 ⋅ 阅读:(13) ⋅ 点赞:(0)

在开发与外部服务、API 或复杂功能交互的应用程序时,测试几乎总是很困难。简化测试的一种方法是使用存根类。以下是我通常使用它们的方法。

福利简介
存根是接口或类的伪实现,用于模拟真实服务的行为。它们允许您:

无需调用外部服务即可测试代码

无需 API 密钥即可在本地工作

通过避免昂贵的 API 调用来加速测试

创建可预测的测试场景

外部会计服务示例
让我们看一个外部会计服务的简单接口。实际上,你甚至不需要接口来实现这一点,但它可以更轻松地切换实现并保持同步。

interface ExternalAccountingInterface
{
    public function createRecord(array $data): string;
}

以下是调用外部 API 的实际实现:

class ExternalAccounting implements ExternalAccountingInterface
{
    public function __construct(
        private readonly HttpClient $client,
        private readonly string $apiKey,
    ) {}

    public function createRecord(array $data): string
    {
        $response = $this->client->post("https://api.accounting-service.com/v1/records", [
            'headers' => [
                'Authorization' => "Bearer {$this->apiKey}",
                'Content-Type' => 'application/json',
            ],
            'json' => $data,
        ]);

        $responseData = json_decode($response->getBody(), true);

        return $responseData['record_id'];
    }
}

现在,这里有一个用于测试的虚假实现:

class FakeExternalAccounting implements ExternalAccountingInterface
{
    private array $createdRecords = [];
    private bool $hasEnoughCredits = true;

    public function createRecord(array $data): string
    {
        if (! $this->hasEnoughCredits) {
            throw new InsufficientCreditsException("Not enough credits to create a record");
        }

        $recordId = Str::uuid();

        $this->createdRecords[$recordId] = $data;

        return $recordId;
    }

    // Edge case simulation
    public function withNotEnoughCredits(): self
    {
        $this->hasEnoughCredits = false;
        return $this;
    }

    // Helper methods for assertions
    public function assertRecordsCreated(array $eventData): void
    {
        Assert::assertContains(
            $eventData,
            $this->createdRecords,
            'Failed asserting that the record was created with the correct data.'
        );
    }

    public function assertNothingCreated(): void
    {
        Assert::assertEmpty($this->createdRecords, 'Records were created unexpectedly.');
    }
}

之前和之后:重构以使用存根
之前:使用 Mockery

public function testCreateAccountingRecord(): void
{
    // Create a mock using Mockery
    $accountingMock = $this->mock(ExternalAccountingInterface::class);

    // Set expectations
    $accountingMock->shouldReceive('createRecord')
        ->once()
        ->with(Mockery::on(function ($data) {
            return isset($data['type']) && $data['type'] === 'invoice' &&
                   isset($data['amount']) && $data['amount'] === 99.99;
        }))
        ->andReturn('rec_123456');

    // Bind the mock
    $this->swap(ExternalAccountingInterface::class, $accountingMock);

    // Execute the test
    $response = $this->post('/api/invoices', [
        'product_id' => 'prod_123',
        'amount' => 99.99,
    ]);

    // Assert the response
    $response->assertStatus(200);
    $response->assertJson(['success' => true]);
}

之后:使用存根

public function testCreateAccountingRecord(): void
{
    // Create an instance of our custom stub
    $fakeAccounting = new FakeExternalAccounting;

    // Bind the stub
    $this->swap(ExternalAccountingInterface::class, $fakeAccounting);

    // Execute the test
    $response = $this->post('/api/invoices', [
        'product_id' => 'prod_123',
        'amount' => 99.99,
    ]);

    // Assert the response
    $response->assertStatus(200);
    $response->assertJson(['success' => true]);

    // Assert that records were created with the expected data
    $fakeAccounting->assertRecordsCreated([
        'type' => 'invoice',
        'amount' => 99.99,
    ]);
}

自定义存根可以轻松测试边缘情况和错误场景:

public function testInvoiceFailsWhenNotEnoughCredits(): void
{
    // Create an instance of our custom stub
    $fakeAccounting = new FakeExternalAccounting;

    // Configure the stub to simulate not enough credits
    $fakeAccounting->withNotEnoughCredits();

    // Bind the stub
    $this->swap(ExternalAccountingInterface::class, $fakeAccounting);

    // Execute the test expecting a failure
    $response = $this->post('/api/invoices', [
        'product_id' => 'prod_123',
        'amount' => 99.99,
    ]);

    // Assert the response handles the failure correctly
    $response->assertStatus(422);
    $response->assertJson(['error' => 'Insufficient credits']);

    // Assert that no records were created
    $fakeAccounting->assertNothingCreated();
}

通过此设置,您的本地开发环境将使用虚假实现,让您无需 API 密钥即可工作,也不用担心速率限制。当部署到暂存区或生产环境时,应用程序将使用真实的实现

查看