Trick to prevent unit tests from touching the database in Laravel
Stop letting your unit tests interact with DB unknowingly and slow tests down
Photo by Patrick Campanale on Unsplash
Before we get into the discussion (read: controversy) on whether unit tests should touch the DB, let me tell you it is not required that your unit tests cannot touch the database. It depends on your testing strategy. Some people are okay with touching the database in unit tests.
We, at Freshbits, isolate unit tests completely. If something needs to connect with the database, that lands in the Feature
tests directory even though we are not making any HTTP requests to test the features at hand.
I know it sounds weird, but that's our testing strategy and is working well for us at the moment.
Preface
By default in Laravel, the tests inside the Unit
directory run before the Feature
tests as per the phpunit.xml
file configuration.
So when you run the tests for the very first time, the database has no tables and that is what we want. But the next time you run them, the database will have the tables because the feature tests have run the migrations.
The problem
Some of our Unit
tests were playing with the database, without our knowledge, because we missed mocking respective classes. Initially, there were no test failures. We were missing out on the test running speed though.
But, once we started running tests on GitHub Actions, unit tests were failing because CI starts with a fresh instance every time.
While we updated those unit tests accordingly, the main problem was that tests weren't failing on the local server but on the CI.
The solution
Wipe the database before running the first Unit
test. The feature tests are using the LazilyRefreshDatabase trait anyway so they will have all the tables they need.
Lots of theory. Let's look at the code. I will show the sample code using PEST as that is what we are using but the fundamentals are the same so this works with PHPUnit too.
Step 1
We use a separate TestCase classes for Unit
tests and Feature
tests. We do that in Pest.php
file this way:
uses(UnitTestCase::class)->in('Unit');
uses(FeatureTestCase::class)->in('Feature');
// ...
Step 2
Next, add the code to wipe the database before running our unit tests. Here is how our UnitTestCase.php
file looks:
<?php
declare(strict_types=1);
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Artisan;
abstract class UnitTestCase extends BaseTestCase
{
use CreatesApplication;
private static bool $dbWiped = false;
protected function setUp(): void
{
parent::setUp();
if (static::$dbWiped) {
return;
}
if ('testing' === app()->environment()) {
Artisan::call('db:wipe');
}
static::$dbWiped = true;
}
}
This is self-explanatory. We are running the Laravel-provided db:wipe
Artisan command to remove all the tables before the first unit test runs.
Peace
That's it. We covered one of the ways to prevent Unit tests from touching the database. There may be other ways too. Feel free to comment below if you have another better way to achieve the same.
Repeating again; It is not required to keep the DB untouched in your Unit tests. It depends on your testing strategy.
If you're someone who has not started writing automated tests yet, here is our short article to kickstart the journey.
Happy testing!