In Laravel, how do phpunit.xml <env> entries, a .env.testing file and the regular .env decide which database a test run touches?
answer
- who sets the variable first wins
- APP_ENV=testing selects .env.testing
- .env.testing replaces .env, never merges
- immutable dotenv repository
- cached config skips .env loading
basics
~20 sphpunit.xml sets APP_ENV=testing and its other <env> values before Laravel boots. Laravel then loads .env.testing instead of .env if it exists, never overwriting variables already set, so phpunit.xml beats .env.testing, unless a cached config bypasses all of it.
solid answer
~40 sPHPUnit applies `phpunit.xml`'s `<env>` entries first, but only for variables not already set in the shell, unless an entry has `force="true"`. The Laravel 13 skeleton sets `APP_ENV=testing`, `DB_CONNECTION=sqlite`, `DB_DATABASE=:memory:` and an empty `DB_URL`. When Laravel boots, it reads `APP_ENV` and, if `.env.testing` exists, loads it **instead of** `.env`: the files are not merged, so keys missing from `.env.testing` (such as `APP_KEY`) are simply absent. Laravel's dotenv repository is **immutable**, so a file never overwrites a variable already set. Precedence is therefore shell or CI variables, then `phpunit.xml`, then `.env.testing` (or `.env`). The trap above all of it is a cached config: if `bootstrap/cache/config.php` exists, Laravel skips `.env` loading and uses the cached values, so tests can reach the development database.
code
ini · 9 lines# .env.testing - loaded INSTEAD of .env when APP_ENV=testing
APP_NAME=BoxClub
APP_ENV=testing
# copy APP_KEY from .env: nothing from .env is merged in
APP_KEY=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=boxclub_test
DB_USERNAME=boxclubgo deeper
Recall that phpunit.xml sets APP_ENV=testing and the test database, and that .env.testing is a separate file tests can use.
Explain the chain: shell, then phpunit.xml without force, then .env.testing replacing .env, with the immutable repository deciding every tie.
Diagnose the wiped development database from a cached config, check CI-exported variables, and make composer test the team's command.
Set a policy for where test configuration lives, phpunit.xml for shared values and CI variables only on purpose, so no environment can point a reset trait at real data.
## The order things happen in Environment values reach a Laravel test through a chain of steps, and each step only fills in what is still missing: 1. **The shell or CI job.** Variables exported before the command runs are already present when PHP starts. 2. **PHPUnit reads `phpunit.xml`.** For each `<env name="..." value="..."/>` it calls `putenv()` and sets `$_ENV`, but only if that variable is not already set. An entry with `force="true"` overwrites anyway. 3. **Laravel boots the application** in `setUp()`. Its `LoadEnvironmentVariables` bootstrapper decides which dotenv file to read. 4. **Laravel reads config files**, whose `env()` calls see whatever the previous steps left behind. ## What the Laravel 13 skeleton's phpunit.xml sets ```xml <env name="APP_ENV" value="testing"/> <env name="BCRYPT_ROUNDS" value="4"/> <env name="CACHE_STORE" value="array"/> <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/> <env name="DB_URL" value=""/> <env name="MAIL_MAILER" value="array"/> <env name="QUEUE_CONNECTION" value="sync"/> <env name="SESSION_DRIVER" value="array"/> ``` The empty `DB_URL` is deliberate: the database config reads a `url` key from `DB_URL`, and a connection URL left over from the environment would otherwise point tests at a real server. The skeleton also turns Pulse, Telescope and Nightwatch off and sets a file maintenance driver. ## How Laravel picks the dotenv file `LoadEnvironmentVariables` does three things, in this order: - If the configuration is cached, it **returns immediately** and loads no dotenv file at all. - If an Artisan command was run with `--env=testing`, it looks for `.env.testing`. - Otherwise it reads `APP_ENV`, which `phpunit.xml` set to `testing`, and switches to `.env.testing` **if that file exists**. If it does not, the ordinary `.env` is used. Two consequences are easy to miss, and two more follow from the chain above: | Belief | What actually happens | |---|---| | `.env.testing` overrides a few keys of `.env` | `.env.testing` **replaces** `.env`; nothing from `.env` is loaded | | `.env.testing` beats `phpunit.xml` | the dotenv repository is **immutable**, so values already set by `phpunit.xml` or the shell win | | `phpunit.xml` beats everything | a variable exported in the shell or CI job wins unless the entry has `force="true"` | | env values always apply | a cached config skips dotenv loading and ignores them | Because `.env.testing` replaces `.env`, it must carry every key the app needs under test. A missing `APP_KEY` shows up as `MissingAppKeyException` the first time something is encrypted, such as a cookie on a feature-test response. ## The classic leak: tests against the development database Picture a subscription-box app whose developer ran `php artisan config:cache` while debugging a production-like setup. The next `php artisan test` boots with the cached `bootstrap/cache/config.php`. That file was built from `.env`, so `database.default` is the local MySQL server and the connection's database is the development schema. The `phpunit.xml` values were placed in the environment, but no config file reads them any more. A test that resets the database now does so against the development data, and the boxes, customers and orders tables come back empty. The defences, cheapest first: - run tests through `composer test`, whose skeleton script clears the config cache before `php artisan test`; - never cache config on a development machine; - keep the in-memory SQLite settings in `phpunit.xml`, or point `.env.testing` at a dedicated test schema; - in CI, check which variables the job exports, because they silently beat `phpunit.xml`. ## Checking it from inside a test When the source of a value is unclear, ask the booted application directly: 1. `app()->environment()` should return `testing`; anything else means `APP_ENV` came from somewhere unexpected. 2. `app()->configurationIsCached()` should be `false`; `true` means every env value is being ignored. 3. `config('database.default')` and the matching connection's `database` value show exactly which database a reset trait would touch. Two minutes spent on these checks is cheaper than restoring a development database from memory. ## Where each value should live - **`phpunit.xml`**: values that must be the same for every developer and in CI, such as array drivers, `QUEUE_CONNECTION=sync` and the test database. - **`.env.testing`**: a complete environment for tests when the app needs many values, or for running Artisan commands with `--env=testing`, such as migrating a test schema. - **The shell or CI job**: machine-specific overrides, used knowingly because they outrank the file. An interviewer asking "why did my tests wipe my local database?" wants the cached-config explanation and the precedence chain, not a general lecture on environment variables.
- The CI job exports DB_CONNECTION=pgsql, but phpunit.xml sets sqlite; which one do Laravel tests use?`pgsql`. PHPUnit only applies a `phpunit.xml` `<env>` entry when the variable is not already set, so the job's exported value survives. Adding `force="true"` to the entry makes `phpunit.xml` overwrite it. Laravel's dotenv loading cannot change it either, because its repository never overwrites existing variables.
- Why does a feature test throw MissingAppKeyException only after a .env.testing file was added?Laravel loads `.env.testing` instead of `.env`, not on top of it. If the new file lacks `APP_KEY`, the key is simply absent in tests, and the first encryption, such as encrypting a response cookie, throws `MissingAppKeyException`. Copy every required key into `.env.testing` or set it in `phpunit.xml`.
- How can you prove which database a test run is using?Inside a test, dump `config('database.default')` and the matching connection's `database` value, and check `app()->configurationIsCached()`. If config is cached, the values came from `bootstrap/cache/config.php`, not from `phpunit.xml`, which explains a leak to the development database.
saying these in an interview costs you the question
- .env.testing is merged on top of .env, overriding only the keys it defines.
- .env.testing overrides values that phpunit.xml already set.
- phpunit.xml <env> values always beat variables exported in the shell.
- A cached config has no effect on tests because phpunit.xml sets APP_ENV.
- Tests always use SQLite in memory whatever the configuration says.