Testing Laravel Environment Checks

Published 25/01/2022 | 6005 views

Ever wondered how to go about testing environment checks in your Laravel application? This post is for you!

How on earth do we go about testing environment checks?

This is in some ways a counterpart to Michael Dyrynda's screencast on this subject. If you're a visual learner, you can watch that by clicking here.

A couple of weeks back, Michael asked a simple question on a Laravel group chat: "What's the best way to run a Pest test against a certain environment?"

For the uninitiated, whenever you run one or more tests in your test suite, either via php artisan test, vendor/bin/pest or vendor/bin/phpunit, Laravel will automatically set the app.environment config value to testing. This is very useful, as it allows us the ability to prevent testing code from running in real application code.

The premise

However, consider the following scenario. We have an app that uses multi-tenancy. Each tenant (or team) has their own database. In order to help ensure that this functionality is working correctly in local, your blade layout might include something like this.

@env('local')
<div>Active Database: {{ DB::connection()->getDatabaseName() }}</div>
@endenv

With this in place, we'd be able to very quickly verify that we're connected to the correct database and get on with our merry dev lives. However, you almost certainly want to avoid exposing this information in production. That could prove to be a real security issue.

In fact, such information is such a risk that it would be wise to write a test for it.

First test attempt

What might such a test look like?

it('shows the current database name in local', function() {
config()->set('app.environment', 'local');
 
$this->get('/')->assertSee('Active Database:');
});
 
it('does not show the current database name in production', function() {
config()->set('app.environment', 'production');
 
$this->get('/')->assertDontSee('Active Database:');
});

You may expect this to work, but it won't. That's because by the time we alter the app.environment value, Laravel has already read and set this value in the application singleton. In other words, it's too late. We need another solution.

Mocking the App facade

One possible solution would be to use the outstanding mocking facilities available on facades to make Laravel "think" we're in production or local, when in actual fact, we aren't. Here's how that would look in practice.

it('shows the current database name in local', function() {
App::partialMock()->shouldReceive('environment')->andReturn('local');
 
$this->get('/')->assertSee('Active Database:');
});
 
it('does not show the current database name in production', function() {
App::partialMock()->shouldReceive('environment')->andReturn('production');
 
$this->get('/')->assertDontSee('Active Database:');
});

Notice that we use partialMock instead of mock. Using mock will cause a whole heap of errors because all of the other methods available on the App facade won't have a clue how to resolve.

There is a problem with this approach. The Laravel application, for the entirety of the test, will think it is in a production environment. This could lead to undesireable side effects, such as using real keys and secrets for services or actually sending messages to that slack integration we have. No bueno.

Creating our own facade

Let's look at another solution. What if, instead of relying on @env blade directive here, we instead create our own class used for checks like this? I like to name this class after the application itself. So, for this site, we'd create a file called app/DowningTech.php.

namespace App;
 
class DowningTech
{
public function usingLocalEnvironment(): bool
{
return App::environment() === 'local';
}
}

We can add other checks over time to this class to help in similar situations. Next, we need to wrap this in a facade. Create a new file at app/Facades/DowningTech.php. I've dived deeper into facades in this post, so I won't elaborate again. Suffice to say that the facade would probably look like this.

namespace App\Facades;
 
use Illuminate\Support\Facades\Facade;
 
class DowningTech extends Facade
{
public function getFacadeAccessor()
{
return App\DowningTech::class;
}
}

Note that by returning the base FQCN in getFacadeAccessor, we don't have to create a binding in our service provider.

Seen as we'll be using this in blade, I would also suggest adding an alias for this facade in config/app.php.

/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
 
'aliases' => [
// ...after the other aliases
'DowningTech' => App\Facades\DowningTech::class,
]

With that done, we can now update our original blade file to use our new facade.

@if(DowningTech::usingLocalEnvironment())
<div>Active Database: {{ DB::connection()->getDatabaseName() }}</div>
@endif

I think this reads really nicely and explicitly informs us of intent. And now, instead of mocking the entire application, we can just mock our custom facade in the tests.

it('shows the current database name in local', function() {
DowningTech::partialMock()->shouldReceive('usingLocalEnvironment')->andReturnTrue();
 
$this->get('/')->assertSee('Active Database:');
});
 
it('does not show the current database name in production', function() {
DowningTech::partialMock()->shouldReceive('usingLocalEnvironment')->andReturnFalse();
 
$this->get('/')->assertDontSee('Active Database:');
});

Of course, if this were a common check, we could create a function in our Pest.php file that performs this step for us.

function useLocalEnvironment(bool $useLocal = true)
{
DowningTech::partialMock()
->shouldReceive('usingLocalEnvironment')
->andReturn($useLocal);
}

Our updated tests would then look as follows.

it('shows the current database name in local', function() {
$this->get('/')->assertSee('Active Database:');
})->useLocalEnvironment();
 
it('does not show the current database name in production', function() {
$this->get('/')->assertDontSee('Active Database:');
})->useLocalEnvironment(false);

Conclusion

So, in summary, the next time you find yourself wanting to test against a particular environment, instead of trying to jump through dangerous hoops, why not create a custom facade?

Kind Regards, Luke

Like what you see?

If you enjoy reading my content, please consider sponsoring me. I don't spend it on cups of coffee; it all goes towards freeing up more of my time to work on open source, tutorials and more posts like this one.