Laravel Envoyer & PHPUnit in Production with a smartly crafted HealthTest

Continuous Deployment is nice, but make sure you deploy a healthy product increment.

Laravel Envoyer is known for deploying PHP applications (especially Laravel or Lumen web applications) with zero-downtime, because of a smart mechanism that performs a full deploy, including all composer packages, migrations and everything your deploy needs, and then, if everything is a success, it just switches a symbolic link of the running website to the newly deployed path. That’s it. It’s a smart hot-swap system that really works.

I find it to be an important piece of the puzzle for any CI pipeline. And this is even smarter when you’re deploying to multiple servers at the same time.

Of course, this is usually not the right platform to run all your unit, browser, integration or any other tests, especially if it targets a production environment.

However, there is one special test that I use to greatly reduce the chances of shipping a broken increment of the product:

The HealthTest

This is a special kind of PHPUnit test, that makes sure the deploy is healthy enough for actual users to interact with it. Although it can run in the usual testing environment, along with all the other tests, in Envoyer I use it for one purpose: to make sure all the moving parts are correctly set up before activating this release.

The HealthTest contains several quick methods that check the following cases:

  • .env entries that need to be there, especially new ones;
  • Database connections such as MySQL, MariaDB, InfluxDB, MongoDB, CouchDB etc.;
  • Amazon SQS queues, DynamoDB or any AWS service;
  • Writing permissions for certain folders, such as cache;
  • Pretty much everything that’s being influenced by the environment and un-staged files;

Consider the following scenario

Development adds a new Amazon SQS queue that’s being used. All the unit and browser tests are passed, because the ‘testing’ environment is mocking all external access. Rumors say it’s a good practice. The development environment has been set up with real, live queues in order to prove that the code actually works. And some weeks later, it’s all being shipped to production. But wait! Nobody bothered to create the production queues.

Guess what!? The new product increment will fail in production because the production queues do not exist. And, it would have been a mistake to have production and development environments use the same queue. Developers would and should, purge the queue at any time, push items that shouldn’t be processed by production code etc.

The testers and the developer might not even notice this until someone complains that something doesn’t work. Here’s where the HealthTest comes into play. As soon as the code passes all checks (via Travis for example), and it’s considered stable to be shipped, Envoyer deploys the code, along with all the packages and everything, and before activating the release, it makes sure the system has everything configured to run in production. How cool is that?

The health check code

Here’s what our HealthTest.php looks like:

It does look like a generic, simple test, with some particularities. Firstly, the code doesn’t use mocks, and it expects for the tests to be run against the production environment. So no adding test data that might show up in user’s accounts, no dropping tables, no migration of anything at all, just making sure all connections are correctly configured and working.

Since your code might use different connections based on the current environment, I’m conditioning some methods only to test if it can run in certain environments (or not in testing at least):

I thought about using markTestSkipped() on the tests that only run in production, but knowingly having the whole test suite saying “OK, but incomplete or skipped tests!” didn’t feel green enough to me.

I love it when tests go either green or red. That yellow thing sounds like an anomaly to me and I avoid it. Especially when I know for sure that those tests will never run against the testing environment, in which tests usually run.

Running the HealthTest

To run this test, make sure you’re not using the phpunit.xml configuration file, which probably sets the testing environment, triggers code-coverage computations or other testing-only configuration options that we don’t want here:

phpunit --no-configuration --color ./tests/HealthTest.php

Always make sure the tests are passed when they are run together with all the others, or you’ll break your testing suite when you run them together with the rest of your tests.

Configuring Envoyer to run the HealthTest

To run the HealthCheck before activating the new release, open your Envoyer project, navigate to Deployment Hooks, click the gear icon next to the Activate Release action, and Add Hook in the Before This Action section.

Lumen Considerations

I have tried the same thing on a Lumen 5.4 framework. And it almost works. Since \Lumen\Lumen\Testing\TestCase always sets the environment to ‘testing’, the above coolness won’t work:

See it there on line 18? Not a really nice thing to do, but it is what it is. For me, at least, it’s unacceptable to hack the core library, so I’ve found a workaround.

The first change updates the HealthTest’s setUp() and tearDown() methods to set the environment to whatever is in the .env file:

This will overload what TestCase::refreshApplication() does. But now, it isn’t safe to run the HealthTest class along with the rest of your tests, as it will always use the environment in the .env file, and this will mess up the Continuous Integration tests at least. So you’ll want to pull it out of the test suite, by editing the phpunit.xml configuration:

Make sure HealthTest.php is not in any of the directories listed in the <directory> tags in your phpunit.xmlfile. Move it outside if you wish. I have it in the tests/ root folder.

Now, whenever you run the full suite of tests, the HealthTest is excluded. When activating the release in production, only run the HealthTest.

You can apply this logic to Laravel as well if you wish, but it’s not mandatory. Laravel works just fine without this particular change.

Conclusion

That’s it! Now you just have to make sure to update the HealthTest whenever a new configuration is added or any of the (usually external) moving parts are changed in any way.

That feeling when you push the code and go out for lunch, resting assured that everything will be alright, is truly priceless!