Configuration precedence when testing Laravel
Main Thread • 6 min read
Recently I set up CI with GitHub Actions. In addition, I added some Laravel Dusk tests. The combination of the two forced me to revisit the precedence of app configuration when testing Laravel.
This is something I covered in Confident Laravel, but wanted to document it in a post. Plus, I found a few more nuances.
While I will cover these in more detail below, the following lists the order of precedence for app configuration when running tests (from highest to lowest).
- In app configuration
- PHPUnit
server
configuration - System environment variables
- PHPUnit
env
configuration .env.testing
file (or.env.dusk
file).env
file
Here is the same listed, represented as code snippets (again ordered from highest to lowest precedence).
config()->set('app.env' 'ci')
<server name="APP_ENV" value="ci"/>
export APP_ENV=ci
<env name="APP_ENV" value="ci"/>
APP_ENV=ci
(within.env.testing
or.env.dusk
)APP_ENV=ci
(within.env
)
This list may provide enough of an idea for you to get started. But I encourage you to continue reading, as there are some important nuances.
PHPUnit server
versus env
configuration
There's actually a difference between the <server>
element and the <env>
element within the PHPUnit configuration file. Those familiar with precedence of PHP superglobals variables may find this obvious. But it can be a bit of a gotcha within the context of a Laravel application. Especially since Laravel changed the default PHPUnit configuration to use <server>
elements.
As such, this was something new I found when writing this post. In fairness, this has more to do with Laravel (and it's use of dotenv) than PHPUnit. That is to say, dotenv gives precedence to server variables over environment variables.
This means the <server>
element effectively sets a PHP $_SERVER
superglobal value. The <env>
element sets an $_ENV
superglobal value. Now setting either of these overwrites what's in the .env
file. So on the surface it seems like it doesn't matter. However, when attempting to overwrite one of these values with a system environment variable, it will only overwrite <env>
elements, but not <server>
elements.
There is actually a force
attribute you may set on these elements. Unfortunately, it does not overwrite system environment variables in Laravel. Again, this is due to dotenv, not PHPUnit.
PHPUnit does not load .env
files
Starting with Laravel 6.0, unit tests began extending PHPUnit's TestCase
class directly. This was done in an effort to improve performance, as well as create a stronger separation between feature and unit tests. Theoretically, unit tests do not need to boot up additional services for the application as they are meant to test code in isolation.
This is a bit of a gray area when it comes to testing applications using a framework. For example, even if I wanted to test a specific method on a model, it may require the database because of underlying Eloquent
code.
However, classes which extend PHPUnit's TestCase
do not boot the application. Therefore, they do not load any .env
files. This may lead to more configuration within phpunit.xml
. In turn, increasing the potential of exposing the nuance we learned above.
Automatic loading of test .env
files
Both Laravel as well as Dusk tests automatically load special .env
files if they exist. Laravel looks for a .env.{environment}
file and Dusk looks for a .env.dusk.{environment}
file. If these exist, they will take precedence over the .env
.
When running Laravel tests, you do not have to set the environment. Laravel defaults to the testing
environment when you run your tests (set through the PHPUnit configuration). Of course you may overwrite this value by setting it through configuration with a higher precedence.
When running Dusk tests, you do have to set the APP_ENV
. By default, this is read from your .env
or system environment variable. This value will be used as the extension to look for a .env.dusk.{environment}
file. Otherwise, it will load the .env.dusk
file, or fallback to the .env
file.
Only Dusk merges .env
files
Since Dusk tests a running application, it expects the .env
file to exist. As such, this is what Dusk uses to determine the APP_ENV
. It then loads any additional .env.dusk
files as described above. Their precedence is:
.env.dusk.{environment}
.env.dusk
.env
Unlike when running Laravel tests, Dusk merges these configuration values. So, you may overwrite only what is necessary for the respective environment within the .env.dusk
files.
Dusk and your app have different configs
One of the things many developers miss about Dusk is that it runs separately. An instance of your application runs in the background, while a completely separate process runs the Dusk tests.
This means the configuration for the app and tests are separate. More technically, the application will run the configuration loaded from your .env
file and system environment variables. So, the precedence for running your application really looks more like this:
- In app config
- System environment variables
.env
file
Notice this does not include configuration from PHPUnit or the special .env.dusk
files.
How I set things up
Okay, now that I have gone through the important nuances, let me share how I now set up my own applications for testing.
I used to use the PHPUnit config as much as possible. This prevented me from having to manage multiple .env
files for different environments.
Furthermore, my app configuration was pretty minimal since I wasn't running CI or Dusk. Now that I am, I want to mirror the production environment as closely as possible.
So I changed the way I typically set things up.
First, I changed all of the <server>
elements in my PHPUnit configuration to <env>
elements. This allows any system environment variables to overwrite those values. It also keeps my local configuration minimal - just PHPUnit overwriting my local .env
.
Next, I created a single .env.ci
file. This contains the configuration for the entire application for the CI environment. Given the extension of this file, it is not used when I run tests locally.
Finally, on the CI, in my case GitHub Actions, I copy the .env.ci
file to be the default .env
file. I set system environment variables for the respective steps to overwrite any specific differences. For example, I change the DB_DRIVER
to mysql
and the APP_URL
for Dusk to the built-in Artisan web server.
I find this set up balances the best of both worlds. I maintain a minimal setup without managing multiple .env
files or complicating my local development workflow. I also leverage precedence to overwrite these values using system environment variables providing a straightforward way to configure the CI environment.
Find this interesting? Let's continue the conversation on Twitter.