Marking a class final is a clear way to say it isn't meant to be extended.
Whether I would choose to do this is a different tip, but a question I hear from developers getting into testing is whether you can still test it.
Yes. A final class can be created and called in a test just like any other class.
$gateway = new PaymentGateway();
self::assertSame(1030, $gateway->totalWithFee(1000));
The trouble starts when you try to mock one.
Laravel's mock() helper uses Mockery under the hood, and Mockery refuses.
$this->mock(PaymentGateway::class);
// Mockery\Exception: The class \App\Services\PaymentGateway is marked final
// and its methods cannot be replaced...
The rest of that error message offers a workaround. You can pass an instance instead of the class name to get a partial mock, "but only if the mock is not subject to type hinting checks."
That last part is the catch.
Mockery normally builds a mock by extending your class, and you can't extend a final class.
Instead, it wraps your instance in a proxy object, which isn't an actual PaymentGateway.
The moment it's passed to a constructor or method that type-hints PaymentGateway, you get a TypeError.
In a Laravel app, that's almost everywhere you'd want to use it.
So what are the real options?
First, ask whether you need a mock at all. If the class doesn't talk to anything outside your app, use the real thing. Your test is more realistic that way.
If it does call something external, like a payment API, put an interface in front of it. Type-hint the interface, bind the real class to it in a service provider, and mock the interface in your tests. The real class can stay final, because Mockery only has to implement the interface, not extend your class.
$this->mock(PaymentGatewayContract::class)
->shouldReceive('charge')
->andReturnFalse();
There's also a package called dg/bypass-finals that strips final from classes as they load during tests.
It works, but I'd treat it as a last resort.
Here to help,
Joel
P.S. Our testing course covers mocking a service, setting up mock return values, and tightening your mock assertions.