Laravel: Testing without booting the app

In other words, making your faster and predictable

Photo by Mohammad Bagher Adib Behrooz on Unsplash

Testing a Laravel application is not something that is slow. Indeed, the recommendation is always to test your app with the application fully booted if your object request something from it.

The problem is that, even if you’re using a single helper function or Facade as part of your logic, then you’re pretty much forced to test with the full application. For example, imagine a class that has one single call to an application service.

The method “exists” is the only logic that requires an application service.

As you can see, the exists() method calls the session to check if a given key exists or not. On the whole class, this is the only Laravel service called, so it feels kinda overkill to boot the whole app just for a single function call.

This time I want to pass on a small hack to bypass the whole Laravel conundrum and just “fake” the service (or object) you’re using, even if you’re calling it from the app through a function or a Facade.

What uses the container?

Laravel comes with multiple helper functions, but the most notorious one are the service functions: cache(), config(), session(), request(), and so on. These are just convenience functions that call the main app() behind the scenes.

The source code of the session helper, which calls the global app function helper.

The session session() helper call different methods on the service retrieved based on the arguments given to. In any case, following the breadcrumbs we end up into an eventual app() call, which is the responsible of returning the service instance.

Here is the main entrance point: the function is just a shortcut to the Container instance singleton. In other words, you can actually “mock” the service you need with Mockery, put it into the container, and let the function do the rest. No need to override a function when PHPUnit bootstraps.

That will cover calling a function, or even the Container directly.

What about facades?

Facades like to play their own game. When you call any Facade method, the base Facade of Laravel will check if the instance it corresponds to was already resolved statically and return it. Otherwise, it will retrieve it as an array member from the container and save it, as seen in the source code.

When a class extends the base Facade, it basically tells the base Facade that all method calls should be redirected to a service name specified by it in the getFacadeAccessor() method.

Trying to use a Facade outside the application, or before it starts, returns the “A facade root has not been set” error. Since the Facade has not been booted, there is no container to resolve the service. The same error can happen if the service wasn’t registered in the Container.

There is a simple solution to fix this problem: just set the app. We can set the container by passing the singleton instance we created before to the Facade::setFacadeApplication() method.

A test setting up a container singleton and into the facade.

The Facade abstract class caches the object inside the $resolvedInstance static array, so any mocked service will persist between tests, which is what we don’t want.

We can easily “reset” the Facade and the services by removing the container instance from it, and then and cleaning the array of cached services. You may do it during the setup of your test, but it’s also valid to do it during the tear down.

With that, we are covered when calling helper functions or Facades, which saves us preferring overridable functions when PHPUnit bootstraps — to me, Facades, Dependency Injection, and functions, is just a matter of preference, not strict functionality, so you shouldn’t need to pick for any of them or be forced to.

With this technique, we can easily bypass booting the whole application — service providers, instances, and whatnot — for small tests that only call a couple of services.

Of course, if there is always a risk of not knowing if the service we’re requesting should be ready or not, but my recommendation is to test first with the full application, and just then move to the no-application test.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store