testo/facade
Testo Facade is a static facade plugin for the Testo PHP testing suite that provides convenient static access to the suite's DI container through the global Testo class. It stores the container when the suite is configured, letting you resolve dependencies anywhere in your tests without passing the...
testo/facade is not a standalone library — it is a plugin for the Testo testing framework (php-testo/testo). Installing it without Testo installed gains you nothing. It exists to give test code static access to the suite's DI container through a global Testo class, which is the closest thing Testo has to Laravel's app() helper.
Minimal setup:
composer require --dev testo/facade
Then make sure the plugin is registered in your Testo suite configuration — installing the package is not the same as enabling the plugin. The README states the plugin "stores the container when the suite is configured," so if it isn't in the plugin list, the facade will have nothing to hand back. Check the main suite config file for the plugin registration array/list.
Where to look first:
https://php-testo.github.io.plugin/facade/. This repo is a read-only sync mirror; the code you see here is generated at release time.plugin/facade/src/ — the README only promises "static access to the container," not a specific method name.First use case worth wiring up: data providers and static helper methods, where constructor injection is impossible.
// A data provider is static — no DI, no $this->container.
public static function userProvider(): iterable
{
$repo = Testo::container()->get(UserRepository::class);
foreach ($repo->allIds() as $id) {
yield "user {$id}" => [$id];
}
}
If you are coming from Pest/Laravel, the mental model is exactly app(SomeService::class) — same convenience, same hazards.
Use the facade as an escape hatch, not the default. Testo resolves test class constructor parameters from the same container. Prefer:
final class CheckoutTest
{
public function __construct(private PaymentGateway $gateway) {}
}
Reach for Testo::container() only when injection genuinely cannot work.
Pattern 1 — static contexts. Data providers, static factory methods on fixture classes, readonly static initializers, and anything instantiated by Testo before the test object exists.
Pattern 2 — test doubles installed once, resolved anywhere. Configure a fake in the suite configuration or in setUp, then let any test or provider pull it out:
protected function setUp(): void
{
Testo::container()
->get(Clock::class)
->freeze(new DateTimeImmutable('2024-01-01 00:00:00'));
}
This works because the container is the suite's container — singletons resolved through it are shared across the run. That is a feature for shared infrastructure (clock, DB connection, HTTP client with a stub transport) and a trap for per-test state.
Pattern 3 — custom plugins and attributes. If you write your own Testo plugin or attribute class, the facade is the idiomatic way to get its dependencies without plumbing the container through every constructor. This is the reason the split package exists; you are consuming the same mechanism Testo's own plugins use.
Pattern 4 — thin wrapper to keep it discoverable. Rather than scattering Testo::container()->get(...) through dozens of files, centralize it:
final class Fixtures
{
public static function db(): Database { return Testo::container()->get(Database::class); }
}
One file to change if the facade API shifts, and grep-able call sites.
Integration tip — container decoration. Because the facade funnels everything through one container instance, registering a decorator/logging proxy in the suite configuration gives you a single chokepoint for "what did this test actually resolve?" That is genuinely useful when debugging a suite whose dependency graph has grown opaque.
Timing is everything. The container is stored when the suite is configured. Anything that runs earlier — a composer autoload file, a bootstrap script, a static property initializer evaluated at class-load, plugin code that executes before the facade plugin's registration — will see an unset container. If Testo::container() throws or returns null, this is the first thing to check, well before you suspect a DI bug.
Package installed ≠ plugin enabled. This trips people up in every split-published plugin ecosystem. Verify the registration in the suite config before debugging anything else.
Keep versions in lockstep with php-testo/testo. The package is split-published from the monorepo plugin/facade/ directory on release. A facade build from a different release line than your Testo core can register against an API that has moved. Pin both together in composer.json and treat upgrades as a single unit.
Global mutable state is test-order dependence. A static container means any test can mutate a shared singleton and silently affect the next one. If you register a fake, restore it — or better, resolve a fresh instance rather than mutating a singleton.
Concurrency. Testo's design is concurrency-oriented. A static container is per-process state; if tests run in parallel workers, each worker gets its own, and anything you assume is "globally set" is only global within that worker. Do not use the facade to coordinate state between tests — it will not work and the failure mode is intermittent.
File issues upstream. This repository is an automatically synchronized read-only mirror. Issues and PRs filed here will be ignored. Go to php-testo/testo/issues and reference plugin/facade/.
Don't make it a habit. The facade is a service locator. Test code that leans on it heavily loses IDE go-to-definition, refactoring safety, and the ability to see a test's dependencies from its signature. A reviewer should be able to ask "why not inject it?" and get an answer.
Reality check on maturity. Zero stars, zero dependents, README-and-CHANGELOG-only maturity. This is internal plumbing for an in-development framework, surfaced as a separate package. It is reasonable to adopt if you are already on Testo; it is not a package to build a Laravel application's testing strategy around, and the API surface may shift between minor releases. Read plugin/facade/src/ before writing against it rather than trusting inference from the one-paragraph README.
How can I help you explore Laravel packages today?