Writing tests in application is really important. Although they increase cost of application at the beginning, in longer run they save a lot of money helping you making changes in existing code base and avoiding serious problems in application after changes you made.
However good tests should in my opinion should:
- test many details of application. I often saw tests that were testing almost nothing or they were passing in case of functionality change (or even worse in case of bugs)
- be as quick as possible – when you often run tests you would like them to be running as quick as possible. You don’t want your tests running for hours.
Of course keeping attention to details is opposite of making tests fast. The more things you test or the most tests scenarios you make, tests are taking more and more time.
In almost each application you should have tests that are using database. When your app is using MySQL and have complex migrations you have almost no choice – you also need to use MySQL for your tests. Using database has of course impact on your tests speed.
In Laravel when using database in tests we have 3 options:
- we can use DatabaseTransactions trait (and before running tests database needs to be migrated). From my experience this is the best option if you have a lot of tests and you care about speed and/or run many single tests
- we can use RefreshDatabase trait – this is also quite quick however at the beginning it automatically migrates database. In case you often run single tests and you care about performance this could not be solution for you. However for running full tests suite this is very good option
- we can use DatabaseMigrations trait – this is the worse choice in my opinion. Migrations are applied before each test and then they are rolled back after each test so the speed of tests is not very impressive.
So using DatabaseMigrations trait for tests is usually the worst choice however in some cases this is the only option you have. If you want to add to your application Browser tests using Laravel Dusk, you need to use them – you cannot use DatabaseTransactions or RefreshDatabase traits because they are both using transactions and your Laravel Dusk tests simply won’t work.
However Laravel Dusk tests are not very fast (comparing to unit tests or even to Laravel tests running http requests). If you add to them using migrations, then performance might be not very impressive. However I found the way how to make Laravel Dusk tests 3 times faster than they are by default.
First of all, let’s see how DatabaseMigrations trait is implemented in Laravel 5.6
1 2 3 4 5 6 7 8 9 10 11 12 |
public function runDatabaseMigrations() { $this->artisan('migrate:fresh'); $this->app[Kernel::class]->setArtisan(null); $this->beforeApplicationDestroyed(function () { $this->artisan('migrate:rollback'); RefreshDatabaseState::$migrated = false; }); } |
As you see first migrate:fresh is running that removes all migrations (quite quickly) and then apply of them (might be slow) but finally after tests all migrations are rolled back (notice migrate:rollback). Although looking from tests speed perspective making this rollback doesn’t make much sense and affects tests performance a lot, in some cases people might want to have database clear after running tests.
So first thing we could do is creating custom trait, that doesn’t execute this line:
1 |
$this->artisan('migrate:rollback'); |
at all. In my tests scenario it would save about 30% of original tests speed. So assuming my tests would take 10 minutes by default, removing this single would change decrease time to 7 minutes.
However tests are still not so fast as they could be. I was testing it in some real application I develop at the moment. I have 53 Laravel Dusk tests with 890 assertions running in Docker container. My application has 93 migrations. Docker for Mac is not very quick itself so probably in other environment tests would be quicker but those tests originally were taking about 25 minutes. I was testing with above change and I was able to cut time to 17-18 minutes. But as I said it’s still not very impressive.
So I decided to test how it would be like this:
- run all migrations into empty database
- dump database into SQL file
- create 1 migration that just run SQL from this file
So instead of running all migrations I had in my real application, I would just run one with ready database structure. I’ve tested it and was really impressed! Combining this with change I described above decreased time to 8-9 minutes so after changes I made my tests are now about 3 times faster than tests I had at the beginning!
Obviously there are some small side effects of this:
- database is not empty after running those tests – as I said before I really don’t care about it, so for me it doesn’t change anything
- whenever I add migrations (or modify them – during development of course only) I need to create new dump for tests. However this takes me < 1 minute and I save this time just running a few Laravel Dusk tests – so it’s definitely worth it.
If you want to give a try and also improve your tests speed, I wrapped it into Laravel package – Laravel quick migrations for convenient usage. In case this package improved your tests speed, please add comment under this post and write how it affected your tests performance.
Ahmed Waleed
Very nice article thank you. But i have one question you are dumping db structure into sql file is it because migrating database from dumped file is faster then migrating from laravel migration classes ?
Marcin Nabiałek
Yes, when running Dusk you need to use database migrations (cannot use database transactions) what means each time before single test all migrations are run. In my solution only single migration is run that just loads dump to database using DB::unprepared what is much quicker when you have more migrations
cristi mocean
I’m getting PDOException: SQLSTATE[HY000]: General error: 1 near “unsigned”: syntax error
Any idea why ?
Marcin Nabiałek
Hard to really say. Assuming you exported dump in valid way it should work. Are you sure you are not running dump for example from MySQL in other engine for example Sqlite?
cristi mocean
That was exactly it, thanks a lot !
I misunderstood how I’m supposed to use your lib. I assumed it’s fine if I use sqlite even though the dump is MySQL .