ergebnis/test-util
ergebnis/test-util is a lightweight collection of reusable test utilities for PHP projects, designed to reduce boilerplate and make common testing tasks clearer. Useful alongside PHPUnit for writing concise, maintainable tests with consistent helpers and assertions.
ergebnis/test-util is a small, dev-only package from Andreas Möller's ergebnis toolchain. Its headline utility is an ObjectBuilder that lets you construct objects in tests via reflection — typically bypassing or partially satisfying the constructor — so you can cheaply produce fixtures for value objects, DTOs, and entities without writing a bespoke factory for every class.
Minimal steps:
composer require --dev ergebnis/test-util
Then look at, in order:
README.md — the package is tiny and the README is effectively the full manual.src/ — a handful of classes; ObjectBuilder is the one you'll actually use.test/ — the package's own test suite demonstrates the intended API better than the docs.A first use case is replacing repetitive fixture setup:
use function Ergebnis\Test\Util\createObjectBuilder;
$customer = createObjectBuilder(Customer::class)
->withId(Uuid::fromString('00000000-0000-0000-0000-000000000000'))
->withName('Jane Doe')
->build();
Because the package was last released in January 2022 and is archived, treat the pinned version and its README as the source of truth for exact method names before you build abstractions on top of it.
Per-domain fixture helpers. Don't call the builder directly all over your suite. Wrap it once per aggregate:
final class CustomerFactory
{
public static function create(array $overrides = []): Customer
{
return createObjectBuilder(Customer::class)
->withId($overrides['id'] ?? Uuid::uuid4())
->withName($overrides['name'] ?? 'Jane Doe')
->build();
}
}
This gives you sensible defaults, keeps tests readable, and means the archived dependency is only referenced in one place if you later need to swap it out.
Data providers. Combine the builder with PHPUnit data providers to generate matrix cases — each provider row supplies only the attributes relevant to that scenario, and the factory fills the rest.
Named constructors and private constructors. This is where the package earns its keep: classes with private function __construct() plus static named constructors (fromString, create, etc.) are awkward to fixture. A reflection-based builder lets you construct them without threading a named constructor through every variation you want to test.
Collaborators. Built objects are ideal as collaborators passed into the class under test (a Customer into a CustomerRepository, an Order into a pricing service), while you keep real mocks/stubs only for the boundary you're actually asserting on.
Static analysis. Builder return types are usually object or static; if you run PHPStan or Psalm at a high level, add a typed wrapper (as above) or @var annotations at the call site so downstream calls are checked.
"ergebnis/test-util": "x.y.z"), keep it in require-dev only, and don't plan around future PHP/PHPUnit compatibility.composer.json constraints that conflict with PHPUnit 10/11 or newer PHP minors. If resolution fails, copy the single builder class into your own tests/Support namespace — it's small enough that vendoring is a legitimate escape hatch.readonly properties (PHP 8.1+). Initialization semantics for readonly/typed properties via reflection differ from ordinary properties and from earlier PHP versions. Verify behavior on your target PHP before relying on it, especially for readonly promoted constructor properties.Error: Typed property ... must not be accessed before initialization. That error usually points at a missing with…() call in your fixture.with* methods and IDEs. Dynamic setters don't autocomplete and aren't visible to static analysis. Wrapping them in your own factory (see above) fixes both discovery and type safety.createObjectBuilder() helper rather than the class directly, make sure composer dump-autoload has run — the function file is autoloaded via files, and a stale autoloader is a common "class not found" red herring in CI.final; extend by composition (your own factory around it), not inheritance.ReflectionException naming the class or property — read the message before assuming a logic bug. When an object looks wrong, var_dump() immediately after build() rather than chasing it through the assertion.src/; it exists to make test fixtures cheap, and nothing more.How can I help you explore Laravel packages today?