Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Test Foundation Laravel Package

open-dxp/test-foundation

A testing foundation for Laravel packages, offering base test cases, helper traits, and sensible defaults that bootstrap PHPUnit or Pest suites quickly. It standardizes setup so package authors spend less time wiring scaffolding and more time writing assertions.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a shared testing baseline for OpenDXP (formerly Pimcore) Laravel packages and applications. It wraps PHPUnit/Pest and Orchestra Testbench into opinionated base test cases so you don't re-wire bootstrapping per repository.

Start here, in this order:

  1. vendor/open-dxp/test-foundation/README.md — the canonical reference for the available test cases, the browser test case, and the commands. The SKILL.md in your agents directory repeatedly defers to it, so treat it as the source of truth.
  2. The base test case classes in src/ — identify which one your package should extend (EnvironmentTestCase is named explicitly in the conventions; there will be siblings for plain package tests and browser tests).
  3. config/test.yaml — the shared configuration every test inherits. Anything not universally needed does not belong here.

Your first use case, in practice: make one existing package test extend the foundation's test case, drop your hand-rolled setUp() bootstrapping, and confirm the suite still passes with the shared config. That single migration tells you whether the package's defaults match your project's assumptions.

use OpenDxp\TestFoundation\EnvironmentTestCase;

final class WidgetTest extends EnvironmentTestCase
{
    // no setUp() — bootstrapping and the wrapping transaction come from the base
}

Two habits to adopt immediately: use Pest's expect() rather than PHPUnit assertions, and treat every test as running inside a transaction that gets rolled back.

Implementation Patterns

Prepare / act / check. Every test body has three blocks separated by a blank line, exactly one action, and nothing asserted before the action. Preconditions are proven by the result of the action, not by a separate assertion. If you find yourself needing a second action, you need a second test.

it('lists only the published children', function () {
    $parent = Widget::factory()->withChildren(published: 2, unpublished: 1)->create();

    $children = $parent->publishedChildren();

    expect($children)->toHaveCount(2)
        ->and($children->pluck('id'))->not->toContain($parent->children->last()->id);
});

Datasets for the same behaviour, never for different paths. A dataset varies data only; if the keys would describe different flows, it's two tests. Name the cases as noun phrases — 'a document', 'an object' — so failures read as sentences.

Factories carry the vocabulary; stories carry fixed worlds. Reach for factory states (withParent(), withChildren()) instead of raw attribute arrays. When a state you need is missing, add it to the factory — that's the extension point, not a one-off create([...]) in a test. For scenarios that require a stable, fixed set of values, put them in tests/Story/.

Randomness through the faker, lazily. self::faker() for random values; when a random value must land in a factory state, wrap it in a closure — $this->with(fn () => ['name' => self::faker()->word()]) — so it resolves at creation time. uniqid() and rand() are banned outright.

Escalate configuration only when needed. config/test.yaml holds what every test needs. A test that requires more configuration belongs in a test case extending EnvironmentTestCase, with a config file that imports test.yaml. This keeps the shared file small and makes special requirements visible at the class level.

Integration with external services skips, never fails. A test hitting a third-party service should markTestSkipped when the service's environment variable is absent, so local runs stay green without secrets.

Promote shared code by kind, not by convenience.

Artifact Location Rule
Helper function in the test file → tests/Helpers/<subject>.php moves once a second file needs it; does real work; never wraps a getter; never contains an expectation
Repeated check tests/Expectations/ becomes a reusable expectation
Named shape tests/Value/ as final readonly class never a bare associative array
Test double tests/Application/, named for the slot it fills (MockTranslator) never an anonymous class

These four conventions are what make the foundation worthwhile: an anonymous mock or an ad-hoc array is invisible to the next developer, whereas MockTranslator and a readonly value object are greppable and reviewable.

Gotchas and Tips

Run everything in the OpenDXP testkit, not on the host. The conventions are explicit about this before handover. Host-run results and testkit-run results diverge (extensions, config, service availability), and static checks are part of the deal — a green test suite on your machine is not the bar.

Static and global state leaks between tests. Transactions roll back the database, but nothing rolls back a mutated singleton, a changed locale, or a modified global. Restore it in afterEach() or ->after(). This is the most common source of order-dependent flakiness in a foundation-style suite.

->throws() has a narrow licence. Use it only when the entire test is the throwing action. Otherwise expect(fn () => ...)->toThrow(), so the throwing call sits inside the three-block structure like any other action.

Chain, don't repeat. Several checks on one result form one chain with ->and(). Multiple expect() statements on the same subject read as separate facts and hide the causal relationship.

Line-level formatting is not cosmetic. From two calls onward, each call in a builder or check chain gets its own line; arrays with two or more entries get one entry per line; arguments go one per line when any argument isn't a plain value or the line passes 120 characters. A boolean or null whose meaning isn't obvious from the call site must be named — getChildren(includingUnpublished: true).

Debugging left in the tree is a defect. only(), todo(), dd(), and ray() never reach the repository. A focused test silently skips the rest of the suite, which is worse than a failure.

Comments explain reasons only. If a comment restates what the code does, delete it. If the code can't show why, that's exactly what the comment is for.

Extension point for suites that outgrow the defaults. Compose the foundation's traits into your own base test case rather than forking the package's classes — the traits are described as composable specifically so that an application with unusual bootstrapping can still inherit the opinionated parts.

Zero dependents is a real risk signal. With one star and no downstream users, you are the first person to hit most edge cases. Pin the version, and keep your own base test case thin so replacing the foundation later is a contained change rather than a suite-wide rewrite.

The release date reads as 2026-10-29. Treat version pins as authoritative and verify the actual tags in the repository before assuming an API surface; don't infer stability from the description alone. Read the README once fully before writing your first test — the skill rules assume you've internalized which base class and which commands apply to your case.

Before handing over, re-read every changed test, factory, story, and helper against the rules line by line. The review is manual by design; there's no linter for "this dataset describes two different flows."

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource