• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

Simplifying work with custom stubs in Laravel

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
1,481
Баллы
155
Testing is almost always difficult when developing applications that interact with external services, APIs, or complex features. One way to make testing easier is to use stub classes. Here's how I usually work with them.

Quick intro on benefits


Stubs are fake implementations of interfaces or classes that simulate the behavior of real services. They allow you to:

  • Test code without calling external services
  • Work locally without API keys
  • Speed up tests by avoiding expensive API calls
  • Create predictable test scenarios
External Accounting Service Example


Let's look at a simple interface for an external accounting service. In reality, you don't even need an interface to do this, but it makes it easier to swap implementations, and to keep them in sync.


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

Here's a real implementation that would call an external 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("

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

", [
'headers' => [
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
],
'json' => $data,
]);

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

return $responseData['record_id'];
}
}

Now, here's a fake implementation for testing:


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.');
}
}
Before and After: Refactoring to Use Stubs

Before: Using 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]);
}
After: Using the Stub


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,
]);
}
Testing Edge Cases


Custom stubs make it easy to test edge cases and error scenarios:


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();
}
Swapping Stubs in Your Base Test Case


To avoid having to swap implementations in every test, you can set up your stubs in your base test case:


class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();

// Create and register the stub for all tests
$this->swap(ExternalAccountingInterface::class, new FakeExternalAccounting);
}
}

Now in your tests, you can directly use the stub without having to register it, and you don't have to worry about accidentally using the real implementation, and forgetting to swap it.


class InvoiceTest extends TestCase
{
public function testCreateInvoice(): void
{
// The accounting service is already swapped in the base test case
// Just get it from the container
$fakeAccounting = app(ExternalAccountingInterface::class);

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

// Assert the response
$response->assertStatus(200);

// Use the stub's assertion methods
$fakeAccounting->assertRecordsCreated([
'type' => 'invoice',
'amount' => 99.99,
]);
}
}
Using Stubs During Local Development


Custom stubs aren't just useful for testing; they can also improve your local DX. Here's how to use them during development to avoid hitting API rate limits or needing API keys:


// In a service provider
public function register(): void
{
// Only use the mock in local environment
if ($this->app->environment('local')) {
$this->app->bind(ExternalAccountingInterface::class, function () {
return new FakeExternalAccounting;
});
} else {
// Use the real implementation in other environments
$this->app->bind(ExternalAccountingInterface::class, function (Application $app) {
return new ExternalAccounting(
$app->make(HttpClient::class),
config('services.accounting.api_key')
);
});
}
}

With this setup, your local development environment will use the fake implementation, allowing you to work without an API key and without worrying about hitting rate limits. When deployed to staging or production, the application will use the real implementation.

The best part: you can have more than one fake implementation, so you can have a different one for local development and one for running tests.

So try them out!


I'm not going to list any more benefits of using stubs, but let me say, you'll definitely have fun with them ?.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу