Yesterday I shared how we organize our feature tests in Laravel apps. Today, I want to talk about unit tests.
In a default Laravel app, unit tests do not boot the Laravel framework at all. This means you can't access any framework services at all.
While this makes the tests faster, I find it to be too limiting for our needs.
So we create a UnitTestCase
base class which extends Laravel's TestCase
class with one addition.
We add a trait which throws an exception if the database is queried at all.
In our definition, we're okay with a unit test having access to Laravel, but it absolutely cannot talk to the database.
We do that with this simple method, which gets called in every unit test setup:
protected function alertUnwantedDBAccess(): void
{
DB::listen(function ($query) {
throw new DatabaseAccessException($query->sql);
});
}
In addition, a unit test should be testing a single public method and should not require interactions between multiple classes.
Here are some examples of things we'd test as unit tests:
- A method on an enum that formats the value for display
- A boolean function on a model indicating if a model is in a certain state
- A custom validation rule wired up to a data provider to test a bunch of edge cases
This is just a sampling, but the thing to remember is that a unit test is testing a single method in isolation without any side effects. Data goes in, data comes out, and that's it.
If you counted the number of tests in our app, unit tests would by far be the smallest percentage. They serve a purpose, but we lean much more heavily on feature tests.
At this point, maybe you're wondering about all the logic to be tested that isn't a request/command or a simple method to test in isolation.
Stay tuned for tomorrow, where I share the third top-level folder we have in every Laravel app: tests/Integration
.
Here to help,
Joel
P.S. Check out the Mastering Laravel community. If you don't find it useful, we'll refund your money.