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).

  1. In app configuration
  2. PHPUnit server configuration
  3. System environment variables
  4. PHPUnit env configuration
  5. .env.testing file (or .env.dusk file)
  6. .env file

Here is the same listed, represented as code snippets (again ordered from highest to lowest precedence).

  1. config()->set('app.env' 'ci')
  2. <server name="APP_ENV" value="ci"/>
  3. export APP_ENV=ci
  4. <env name="APP_ENV" value="ci"/>
  5. APP_ENV=ci (within .env.testing or .env.dusk)
  6. 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.