Back

Settings in tests

๐Ÿงญ

Is this package for you?

17 reasons, each a before and an after. Select the problems you have and see how many match.

Take the scorecard
๐Ÿงช

Change one value without restating the other seven

A test that cares about one timeout should not have to name every other property to get there โ€” and it should not break the day someone adds an eighth.

โœ• Every argument, every time
$this->app->singleton(AuthSettings::class, fn () =>
    new AuthSettings(
        domain: 'test.example.com',
        redirect_url: 'http://test.example.com/cb',
        timeout: 5,        // the only one this test cares about
        mfa_enabled: false,
    ));

Leave one out and you get ArgumentCountError.

โœ“ Only what the test is about
$this->app->singleton(AuthSettings::class, fn () =>
    AuthSettings::fake(['timeout' => 5]));

// the other properties keep their real values,
// and a new one added next month changes nothing here

The test states its subject and nothing else.

The real cost of the left-hand column is later. Add a property to the class and every test that swapped it breaks at once โ€” none of which cared about the new property. That is a tax on exactly the discipline this package exists to encourage.

Three ways in

Pick a call to see what it returns. Every value below came from running it against this demo's own AiSettings.

AiSettings::resolve()

what the app gets, honouring APP_ENV

provider openai unchanged
text_model gpt-4o unchanged
max_tokens 8000 unchanged
AiSettings::from('staging')

any environment, whatever APP_ENV says

provider openai unchanged
text_model gpt-4o-mini differs from resolve()
max_tokens 2000 differs from resolve()
AiSettings::fake(['max_tokens' => 10])

one value changed, the rest left alone

provider openai unchanged
text_model gpt-4o unchanged
max_tokens 10 differs from resolve()
AiSettings::fake(['provider' => 'azure'], 'development')

start from another environment, then change one value

provider azure differs from resolve()
text_model llama3.2 differs from resolve()
max_tokens 1000 differs from resolve()
AiSettings::from('production')->with(['temperature' => 0.0])

derive a copy from an instance you already hold

provider openai unchanged
text_model gpt-4o unchanged
temperature 0.0 differs from resolve()

What each one is for

from(string $env)

Resolve any environment on demand

Ignores APP_ENV entirely. Useful for asserting that production really does use the values you think it does โ€” from a test running under testing.

fake(array $props, ?string $from)

A double with only the differences named

Starts from the current environment, or the one you name, then replaces just the properties you list.

with(array $props)

Derive a copy from an instance

Called on an object you already hold rather than on the class, so you can layer a change onto anything โ€” including the result of from().

๐Ÿงฌ

They return the real class

Not a mock and not a stub โ€” an actual AiSettings, fully typed, so anything type-hinting the class accepts it and your assertions read normally.

๐Ÿ”—

They compose

from('production')->with([...]) reads as one sentence: take production, change this. Each call returns a new instance rather than mutating.

In a test

it('stops retrying after the configured limit', function () {
    $this->app->singleton(
        PaymentSettings::class,
        fn () => PaymentSettings::fake(['retry_attempts' => 2]),
    );

    // mode, currency and webhook_url keep their real values
    expect(attemptPayment())->toHaveBeenTried(2);
});
Read the docs All four commands Overrides Is this for you? Back to the demo