Instantly share code, notes, and snippets.

Embed
What would you like to do?
A Better Database Testing Workflow in Laravel
<?php
use Illuminate\Contracts\Console\Kernel;
trait DatabaseSetup
{
protected static $migrated = false;
public function setupDatabase()
{
if ($this->isInMemory()) {
$this->setupInMemoryDatabase();
} else {
$this->setupTestDatabase();
}
}
protected function isInMemory()
{
return config('database.connections')[config('database.default')]['database'] == ':memory:';
}
protected function setupInMemoryDatabase()
{
$this->artisan('migrate');
$this->app[Kernel::class]->setArtisan(null);
}
protected function setupTestDatabase()
{
if (! static::$migrated) {
$this->artisan('migrate:refresh');
$this->app[Kernel::class]->setArtisan(null);
static::$migrated = true;
}
$this->beginDatabaseTransaction();
}
public function beginDatabaseTransaction()
{
$database = $this->app->make('db');
foreach ($this->connectionsToTransact() as $name) {
$database->connection($name)->beginTransaction();
}
$this->beforeApplicationDestroyed(function () use ($database) {
foreach ($this->connectionsToTransact() as $name) {
$database->connection($name)->rollBack();
}
});
}
protected function connectionsToTransact()
{
return property_exists($this, 'connectionsToTransact')
? $this->connectionsToTransact : [null];
}
}
<?php
abstract class TestCase extends Illuminate\Foundation\Testing\TestCase
{
use DatabaseSetup;
/**
* The base URL to use while testing the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
protected function setUp()
{
parent::setUp();
$this->setupDatabase();
}
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
}
@rokde

This comment has been minimized.

rokde commented Nov 16, 2016

nice

@luisdalmolin

This comment has been minimized.

luisdalmolin commented Nov 16, 2016

awesome!

@RomainSauvaire

This comment has been minimized.

RomainSauvaire commented Nov 17, 2016

Great work Adam !

@mattwells

This comment has been minimized.

mattwells commented Nov 17, 2016

Hey Adam, I really love this idea but something I have been bitten by before is with a CI machine that works with multiple branches which may have different migrations, if you run migrate:refresh on a different branch and it tries to roll back something that doesn't exist as a migration it will fail. It might be useful to have a migrate:rollback running at the end. Do destructors work on traits?

@cmosguy

This comment has been minimized.

cmosguy commented Nov 17, 2016

@adamwathan this is so rad. If I am running windows, I need mysql on it running, correct?

@jago86

This comment has been minimized.

jago86 commented Nov 22, 2016

Great! I love it

@coderua

This comment has been minimized.

coderua commented Jan 15, 2017

Adam, great thanks, that's very very helpful!

@AdamEsterle

This comment has been minimized.

AdamEsterle commented Feb 11, 2017

@adamwathan great idea and trait!

  1. Some people talk about about how the "right way" is to re-migrate your DB after every test to make sure there are no bugs/interdependence. Thoughts? It seems that a transaction will be fine, unless there is a bug with the MySQL Transaction itself (obviously outside of our control)

  2. Just to verify, this will be only be helpful for an actual testing connection; an in-memory db will be the same whether using DatabaseMigration or DatabaseSetup, correct?

  3. You mention in the video https://adamwathan.me/2016/11/14/a-better-database-testing-workflow-in-laravel that using transactions are faster than an in memory db as your times show. But mine are opposite, transactions are slightly slower than in-memory. Any idea as to why?

@Garbee

This comment has been minimized.

Garbee commented Mar 13, 2017

Some people talk about about how the "right way" is to re-migrate your DB after every test to make sure there are no bugs/interdependence. Thoughts? It seems that a transaction will be fine, unless there is a bug with the MySQL Transaction itself (obviously outside of our control)

Transactions keep the database clean by not persisting any modified data in between test runs. Remigrating is useless, since you're just dumping the empty tables and setting them all back up again. You're just taking extra time to remigrate for no gain in the tests. Which becomes a major problem once you have a long chain of migrations to run through over time.

Remigrating fully is a hack to not use transactions to keep things stable. If you wanted to be purist on "remigrating after every run" then you should do a dump of the clean tables and restore from a dump file and not even run the migration system for tests at all. That is the best speed without involving transactions.

Just to verify, this will be only be helpful for an actual testing connection; an in-memory db will be the same whether using DatabaseMigration or DatabaseSetup, correct?

This isn't making much sense to me. You aren't ever testing connections (since if they fail the tests will fail), you're testing whether the application running hits the given DB as needed.

You mention in the video that using transactions are faster than an in memory db as your times show. But mine are opposite, transactions are slightly slower than in-memory. Any idea as to why?

As with anything towards performance... It depends™. Your number of tables, migrations, HDD/SSD access speed, RAM access speed, CPU capacity and current workload, etc. can all affect what is going on. In his video with his machine running as it was, all combined to make his memory version slower. Your results may be different between any set of given machines with the exact same test setup. We're talking a few hundred milliseconds at worst here in most normal environments, that isn't too much to worry about in either direction.

@jesseleite

This comment has been minimized.

jesseleite commented Mar 24, 2017

You should PR this into Laravel core somehow!

@slava-vishnyakov

This comment has been minimized.

@kz

This comment has been minimized.

kz commented Jul 27, 2017

@adamwathan For learning purposes, can I ask what the technical reason is for $this->app[Kernel::class]->setArtisan(null);?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment