Appearance
Testing
Since essence v0.12.0, essence ships a reusable Orchestra Testbench base class that essence and every companion package test against. It boots a real Laravel app, runs real migrations against an in-memory SQLite database, and drives inbound updates through the actual route → middleware → controller stack. The only thing faked is the outbound Telegram API.
The shared TestCase
TelegramBotEssentials\Essence\Testing\TestCase is available to any package through its existing essence dependency — no extra requires. Extend it and add your own package's service provider (plus any companion it depends on):
php
namespace TelegramBotEssentials\Billing\Tests;
use TelegramBotEssentials\Billing\TbeBillingServiceProvider;
use TelegramBotEssentials\Essence\Testing\TestCase as EssenceTestCase;
use TelegramBotEssentials\Settings\TbeSettingsServiceProvider;
abstract class TestCase extends EssenceTestCase
{
protected function getPackageProviders($app): array
{
return array_merge(parent::getPackageProviders($app), [
TbeSettingsServiceProvider::class,
TbeBillingServiceProvider::class,
]);
}
}What the base sets up:
| Concern | Behaviour |
|---|---|
| Database | sqlite :memory:, connection testing; add RefreshDatabase in tests/Pest.php to migrate per test |
| Cache | pinned to the array store (Workbench's skeleton defaults to database, which has no table) |
| Locale | app.locale / app.fallback_locale both en |
| Outbound HTTP | Http::fake() in setUp() |
Telegram calls are never real
essence's default HTTP handler (LaravelHttpClient) routes every telegram-bot-sdk call through Illuminate\Support\Facades\Http, so the Http::fake() in setUp() intercepts all of them. Tests run with no network, no real bot token, and nothing ever reaches api.telegram.org. No bespoke SDK mocking is needed.
Assert on what would have been sent:
php
$this->assertTelegramSent(fn ($request) => str_contains((string) $request['text'], 'Bot Settings'));
Http::assertNothingSent(); // e.g. a feature-flag-gated handler that bailed earlyHelpers
| Helper | Purpose |
|---|---|
makeBot(array $attributes = []) | A Bot with activated_until 10 years out (a bare factory bot is treated as expired) |
makeBotUser(Bot $bot, int $peerId, array $attributes = []) | Pre-create a bot's user at a Telegram peer id — the only way to test a role-gated handler |
makeMessageUpdate(string $text, ?int $peerId = null, ?int $chatId = null) | A minimal private-chat message update |
makeCallbackQueryUpdate(string $callbackData, ?int $peerId = null, ?int $chatId = null) | A minimal callback_query update |
postWebhookUpdate(Bot $bot, array $update) | Send an update as a real inbound webhook request — routing, TelegramBotAuthentication, and the controller all run |
assertTelegramSent(callable $callback) | Http::assertSent() wrapper |
Full-stack handler test
php
use TelegramBotEssentials\Essence\Enums\Roles;
beforeEach(fn () => replyKeyBus()->addReplyKey(BotSettingsKey::class));
it('opens the settings menu for an admin', function () {
$bot = $this->makeBot();
$this->makeBotUser($bot, 555, ['power' => Roles::ADMIN->value]);
$this->postWebhookUpdate($bot, $this->makeMessageUpdate('Bot Settings ⚙️', peerId: 555))
->assertOk();
$this->assertTelegramSent(fn ($r) => str_contains((string) $r['text'], 'Bot Settings'));
});Service-level test
When you only need the webhook context (not a whole request), set it directly. essence's Webhook accessors otherwise throw when there is no active update:
php
it('defaults the currency setting to USD', function () {
wHook()->setBot($this->makeBot());
expect(app(Currency::class)->getCurrency())->toBe('USD');
});Encrypted settings
A SettingType::SENSITIVE setting is encrypted on write, which needs an app key. The base doesn't set one — override defineEnvironment() in your package's TestCase if you touch sensitive settings:
php
protected function defineEnvironment($app): void
{
parent::defineEnvironment($app);
$app['config']->set('app.key', 'base64:'.base64_encode(random_bytes(32)));
}Package tooling layout
essence and the companions share the same setup:
pint.json # { "preset": "laravel", "exclude": ["vendor"] }
phpstan.neon.dist # larastan, level max, includes phpstan-baseline.neon
phpstan-baseline.neon # generated: composer analyse -- --generate-baseline
phpunit.xml # bootstrap vendor/autoload.php, Feature suite
tests/
├── Pest.php # uses(TestCase::class, RefreshDatabase::class)->in('Feature')
├── TestCase.php
└── Feature/Composer scripts (mirrored across every package):
json
"scripts": {
"test": "pest",
"lint": "pint --test",
"format": "pint",
"analyse": "phpstan analyse"
}CI runs composer test on PHP 8.3 / 8.4 / 8.5, plus composer lint and composer analyse as a separate job.
Was: a PHPStan memory workaround
Earlier in the 0.12 cycle, Larastan booting a package's app during analysis could hit essence's ExceptionHandler recursing into itself (a service reading per-bot settings in its constructor with no active webhook), OOMing the process. Every companion carried a phpstan-bootstrap.php stub and ran analyse with a raised memory_limit to work around it. The handler now guards its fallback path with wHook()->check(), so those stubs have been removed and composer analyse runs plain.
Interactive development with Workbench
Every package (and tbe-package-skeleton) is set up for Laravel Workbench, so you can boot a throwaway app around the package under development:
bash
composer serve # boot the workbench app
vendor/bin/testbench tbe:make:command PingCommand
vendor/bin/testbench migrateThe tbe:make:* generators detect when they run under Testbench and write into the package's own src/ (resolving the target PSR-4 namespace from its composer.json) instead of the throwaway app.