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
AuthSettings::fake(['timeout' => 5]);

// bound as the container singleton for you โ€”
// 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, replaces just the properties you list, and binds the result as the container singleton.

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 () {
    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