17 reasons, each a before and an after. Select the problems you have and see how many match.
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.
$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.
$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.
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() |
from(string $env)
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)
Starts from the current environment, or the one you name, then replaces just the properties you list.
with(array $props)
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().
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.
from('production')->with([...]) reads as one sentence:
take production, change this. Each call returns a new instance rather than mutating.
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);
});