Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign up[6.x] Implement integration test and in-memory DB #5169
Conversation
Great PR infos provided. +1 for that. Setting up the in-memory db is also what I do every time and still I have to look it up more than I like to admit :-) So providing an easier way to use would be awesome. I think the integration directory would also be an improvement. The only thing I would change: I wouldn't take an Eloquent model as an example for a unit test because it can be misleading. Yes, you can use it and it is valid, but as an example I would prefer showing testing something else like a plain PHP class. |
Not a bad idea, but it seems like all the "real" apps I've worked on aren't able to use sqlite because of the DB structure or renaming columns in a migration somewhere. That being said, this would seem to be a good framework default. Perhaps adding an additional callout or section to the docs about considerations to make when swapping to a regular mysql testing DB would also be useful? |
@christophrumpel Heya, thanks! I'm gonna keep this part because it's actually one of the main points I want to make here that you can perfectly unit test Eloquent models. Plus there aren't any other skeleton defaults which can be used in a unit test so I'd rather not have to create a dedicated class for it. |
Maybe add foreign key checks too if database connection is available? |
@hulkur heya. Foreign keys for the sqlite database are enabled by default so there's no need to do separate checks for that: https://github.com/laravel/laravel/blob/master/config/database.php#L43 |
I'm indifferent on the directory changes, but using an in-memory database gives me a little pause. I'll just mention 1 issue I've run into before that prevents me from using SQLite. Without an extra extension, SQLite is unable to use 'json' type columns. |
Personally I just put any integration test in the "Feature" test directory, which was kind of its original purpose. Do you think by introducing a third distinction we are making things more confusing? What's an integration test vs. a feature test? Feature tests are definitely integration tests. Are some integration tests not feature tests? |
No not at all. Let me clarify: the difference, in my eyes, lies in the cycle it undertakes. Integration tests are tests which test components. Think: saving data in a database, dispatching jobs and checking reactions (mocking events, etc). Because you're testing one or only a few components they're faster than feature tests. Feature tests, on the other hand are full cycle tests. You call an endpoint and follow the full request through an app and assert behavior. They touch a lot more parts of the app than just a couple components. Usually it involves lots of components being executed. And thus this makes these tests slower than integration tests. There's a fourth category as well. End-to-End tests, which mimic the behavior of an actual user using your app. Think Dusk tests, actually making payments on a billing page with the testing api of Stripe, saving files on an actual test S3 bucket. This fourth category might be a little much for the default skeleton (mainly also because Dusk scaffolds this group) but I think these three directories definitely have their place. All in all I want to say that there's definitely a clear distinction between Unit, Integration and Feature tests in my eyes. But I'm all open to listen to counter arguments of course |
Let me show you another example. In Laravel.io I've separated the test suite between "Feature", "Component" (Integration) & spec (Unit) tests. Have a look at these different tests to see how they differ, I think you'll get a better picture on what I mean: https://github.com/laravelio/portal/tree/master/tests |
@driesvints I checked your example and i found Component and Feature only. Also i checked one of compnents tests. It seems same idea of Unit Test. |
@ahmad-shawky spec tests are done by PhpSpec and live in the
Which one? |
I also want to note that there's definitely no fixed rule on separating Integration and Feature tests. It's just that I believe they behave differently enough to warrant the separation |
https://github.com/laravelio/portal/blob/master/tests/Components/Models/ReplyTest.php |
@driesvints I understand your idea more clearly now. I think will be good |
Just dropping this here: https://blog.twitter.com/engineering/en_us/topics/insights/2017/the-testing-renaissance.html
I always liked the fact that Laravel avoids that term due to it's ambiguity. Unit tests can also be ambiguous, but in the context of Laravel it was always "if it's not feature tests, its unit test" regardless of what they mean to you or your project. |
@ahmad-shawky that still makes use of a database under the hood, hence why it's an integration test :)
@deleugpn This is something I've always found confusion myself when working on Laravel projects. Hence the PR and the attempt to make things more clear :) |
I understand the sentiment, but just to make sure my post wasn't confusing: having two ambiguous test suite is not better than having only one. |
I would rather see the Integration tests in the feature folder. They're often confused and I don't think it's Laravel's job to introduce further confusion by having them at the same level. Laravel defaulting to |
imo it might be confusing. We're getting trouble to find the difference in this thread. I think if it is really going to be merged it must be clear the difference about the integration and feature tests |
Not fussed about the in-memory db default as we use raw queries in our app (on duplicate key update, for example) and have to test it against mySQL. But can see it being useful for like 80% of scenarios. Love the new Integrations test idea (assuming I'm understanding it right). The way I'm understanding your differentiation is:
Have I understood that right? |
I really like the separation of However, in my experience using SQLite to test your integration against can in some cases lead to false positives. I have encountered multiple issues where my test suite against SQLite ran perfectly fine only to find that I broke my production server which ran on PostgreSQL. I therefore wouldn't personally use that, but I'd say it's fine as a default. You can always change the phpunit settings. Perhaps a Testcase class can be introduced for unit tests, which already includes the |
Agree with @denniskoster and @JackEllis , I'd use MySQL/Postgre for the tests too to be the same as the production database. Tests passing on SQLite then failing on prod gives a false sense of security IMO. |
In my experience people use the If the goal is "make people stop putting their model integration tests in the unit directory", a better structure would be:
+1 on sqlite by default |
My two cents on the sqlite vs your production driver: in my experience you just want to swap the db driver for a fast one to run your tests often. But within your CI environment you can choose your production driver to make sure your tests pass on it before it's deployed to production. Or you can use the production driver on select tests where you know the behavior is different. I wouldn't want my tests to run slowly locally just because I want to make sure it uses the correct driver. That's what CI is for imo. |
100% agree with Default set In-memory DB. |
I agree with others that it should only have two folders. Once you go three it introduces too much analysis paralysis and just adds more confusion. But the rest of the stuff I love 100%! |
What if we just renamed the "Feature" directory to "Integration"? |
default In-memory DB is undoubtedly agreed by all. |
Another possible option would be to add a flag to the Laravel installer and one can explicitly ask the installer to prepare a SQLite based PHPUnit setup or a MySQL/Postgres one. |
Well, renaming "Feature" to "Integration" seems to make no difference. I mean, they are pretty similar imo. About in-memory db as default I think it's a christmas gift lol |
My teams have some confusion over what a "feature" test is, but adding a third folder will add even further confusion IMHO. I wouldn't mind if In other environments (i.e., Rails) an Integration test is similar to what Laravel calls a feature test. The words require some context anyway. Another example where multiple definitions exist: a System test is also called End to End or Functional test in some frameworks. The terms can be a bit vague so I personally like:
|
Why is it easier? Again, I don't necessarily disagree, but I'd like some reasons behind the argument to switch to an in-memory SQLite database. I think we can all agree it's faster. Are there other benefits? Do these benefits outweigh the potential discrepancies between testing on your production database? Are greener users going to be served better with speed or with accuracy? |
I love the idea of having an in-memory database out of the box, it would make spinning up a new app and demoing some tests that use a database a breeze. I do agree with @browner12 that developers should use caution in using a different engine for tests vs. real application. I've been burned by this in the past, but in practice not too often. Not sure if the onus needs to be on the Laravel framework to stop users from using different engines in testing though. Would the Laravel team consider making the default database connection SQLite? The experience out of the box would require creating a development database, but might be a nice default option. I know Rails uses SQLite by default out of the box. Setting up a new Laravel app that has a working database connection with zero database configuration would be awesome IMO. |
Who are we making these defaults for, though? Someone who wants to show some demos, or somebody starting a production ready application?
Maybe there's some pain point I'm missing here? I use Homestead, and it creates the MySQL databases for me. All I need to switch is the "database" env variable. Is it hard to make databases? |
Are the in memory tests really faster? I have swapped MySQL for Sqlite in memory a number of times and always found the difference to be negligable, and sometimes slower. |
Maybe "demo" was the wrong word, but I just mean that it's nice to My thought was that SQLite is a decent development database that you can easily change to other drivers, but could provide a default that makes the following possible, with zero setup friction:
It's just a thought though, just throwing the idea out there since my experience in Rails using SQLite as the default has been a nice experience, and it's easy to change to another driver. |
Personally I think trying to over-categorize things is not really useful/productive and mostly just pedantic procrastination of actually productive work. The term "integration test" is unhelpfully vague IMO, and in practice I have written very few true "pure" unit tests in any Laravel application I've developed. Instead I personally use "unit test" to mean "testing a method on a class" (regardless of how many real collaborators I am using or whether it talks to the file system, database, whatever), and "feature test" to mean "testing an HTTP endpoint". The only other level of categorization I've ever found to be helpful is "browser tests", which is stuff using Dusk in the context of Laravel. To me, trying to categorize your tests based on whether they "integrate" with anything vs. are pure or whatever is just not really helpful, and is a point of friction/bike-shedding more than anything else. Instead I find it much more useful to categorize based on the APIs the tests are using, so all Dusk tests get grouped together, all HTTP tests that use APIs like This makes categorization simple and obvious, and much less subjective. I think the current defaults are totally fine but if they were going to change at all I would probably either have 3 folders named Edit: I wrote this without reading the whole thread and realizing the discussion about new directories is already finished, sorry |
I find myself doing this in every single project because of the speed and the easy it is to start over. Unless you have a really complex query you are fine to go with SQLite in testing. I have a large app with joins and subqueries and tests are built into SQLite. I swap to MySQL when I find a really complex query failing on SQLite , but until that moment I was able to enjoy the SQLite speed breeze. I would like to mention, as a lot of people did before me, that this is only the default configuration. I mean, if you don't agree with this and you prefer testing in MySQL, you still need to change the Apart from using SQLite or MySQL for testing, I would recommend to put some variables in there just to prevent running the tests into your local development database by default. Since there are no variables it will use the ones in the |
I think using different drivers make your tests doubtful. They are supposed to give you confidence that things will not break. I mean, we would not run our tests with PHP 7.4 if I we are deploying 7.3. If both the default connection and the default test connection were to be sqlite, that's a completely different story. I see benefit in having a database solution that works out of the box like we do have a cache driver, sessions driver, queue driver, etc. |
I feel we shouldn't try to carry over the established distinctions between a feature and an integration test to the framework, unless we revisit how we define what a unit test is within a Laravel app (currently that's anything that's not feature). The framework has done a tremendous job in making testing simpler. If anything could use a new label it should be unit imo. I like the sqlite in-memory default |
a few years ago i would probably have agreed with default of in memory sqlite, but after being bit by that i would advise people not to be using it for integration tests. it does not behave the same as other databases (data constraints - or lack of). if you are running a test suite you should make the environment as close as possible to how it will be running in production, otherwise there isnt really much point imo - what good is it to know that you have 100% test coverage and all tests passing on an in memory db if it doesnt work on the database you actually want to use, thats just misleading also adding to this with how widespread docker is spinning up a clean mysql/postgres/mssql etc etc etc db doesnt really take any effort |
Not much time, therefore in short:
If you imagine the test pyramid there are unit tests at a very low level, then integration (feature) tests on top of that and at the very top, there might be a "super feature test" (which I'd call functional test). But the third one is very theoretical. So summary: Rename Update: Saw the directory discussion is already done. So just ignore my pov above ;) |
Merged! Thanks for all your feedback on this one people! |
I just want to say thank you to everyone who contributed to the discussion. It was very useful, educational and genuinely refreshing to see so many people calmly and politely make their point. |
Adding an alias for PHPUnit/Framework/TestCase to make it clear that the TestCase being used isn't the Framework provided TestCase to remove ambiguity for users where IDE's collapse use statements, on the back of the changes made in PR laravel#5169
* Update to Laravel 5.9 * Require PHP 7.2 * Encourage to use PHPUnit 8 * Update changelog * Remove services deleted from core See laravel/framework#28441 and laravel/framework#28442 * formatting * remove dumpserver since doesn't work on 5.9 * remove ui scaffolding * introduce test bootstrapping * style fix * [5.9] Add ThrottleRequests to the priority array (laravel#5057) * add ThrottleRequests to the priority array * Move ThrottleRequests under Authenticate middleware * Update version constraint (laravel#5066) * formatting * [6.0] - Add vapor link on the welcome view (laravel#5072) * Remove deprecated language line (laravel#5074) * formatting * add failed jobs table * Rename 2019_08_19_175727_create_failed_jobs_table.php to 2019_08_19_000000_create_failed_jobs_table.php (laravel#5082) * [6.0] Use phpredis as default Redis client (laravel#5085) * Use phpredis as default Redis client Follow up for laravel/framework#29688 It's best that we already start using `phpredis` as a default to discourage usage of Predis. * Update database.php * add new failed driver option * add ignition * add ignition * Allowing optional use of yml/yaml file extensions in .editorconfig (laravel#5090) * Update bootstrap.php * add phpunit extension * style * Set argon defaults to prevent password_hash(): Memory cost is outside of allowed memory range on PHP 7.4 (laravel#5094) With the values ```` 'argon' => [ 'memory' => 1024, 'threads' => 2, 'time' => 2, ], ``` Hash::make() produces password_hash(): Memory cost is outside of allowed memory range on PHP 7.4 * Revert "Set argon defaults to prevent password_hash(): Memory cost is outside of allowed memory range on PHP 7.4 (laravel#5094)" (laravel#5095) This reverts commit 86908e1. * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * According to PHP Bug 78516 Argon2 requires at least 8KB (laravel#5097) https://bugs.php.net/bug.php?id=78516 Argon2 requires at least 8KB On PHP 7.4 memory 1024 will throw: password_hash(): Memory cost is outside of allowed memory range * Order imports alphabetically * Apply fixes from StyleCI (laravel#5100) * Update CHANGELOG.md * Revert "According to PHP Bug 78516 Argon2 requires at least 8KB (laravel#5097)" (laravel#5102) This reverts commit 74d84e9. * Added Appoly sponsor (laravel#5105) * remove testing bootstrap extension (laravel#5107) * [6.x] Add 'null' logging channel (laravel#5106) * Add 'none' logging channel * Remove extra spaces * Rename 'none' channel to 'null' * Update logging.php * [6.x] Added OP.GG sponsor (laravel#5121) * Added OP.GG sponsor * Update readme.md * Add new password rule language line * Remove middleware from password reset It's not necessary for the user to be logged out when resetting their password. This allows users to reset their password while logged in. Can be used in combination with the new RequiresPassword middleware. * Implement password confirmation * formatting * formatting * Update CHANGELOG.md * Fixes required version of the framework within `composer.json` (laravel#5130) * Add xml schema to phpunit (laravel#5139) This allows an IDE to do auto completion, and show any errors in the configuration * Security fix: Waiting before retrying password reset * tweak formatting * fix key * Update .styleci.yml * Consistent readme * Rename readme * Update readme * Update CHANGELOG.md * Rename `encrypted` to `forceTLS`. (laravel#5159) * Use laravel/tinker v2 (laravel#5161) * Consistent order (laravel#5167) Keep the alphabetical order of the validation messages. * [6.x] Implement integration test and in-memory DB (laravel#5169) * Use in-memory DB for testing * Extend from PHPUnit test case for unit tests * DRY up path (laravel#5173) * Add "none" to supported same site options in session config (laravel#5174) * change some default settings * Update redirectTo return type PHPDoc * Updated config/logging.php (laravel#5179) This adds a default emergency logger path to the logging config file. This change goes hand-in-hand with my changes found here: https://github.com/Stokoe0990/framework/commit/7a03776bc860bde4cdc82e69ab133a755b66dd2d * default email from name to app name (laravel#5178) * Add MAIL_FROM_ADDRESS & MAIL_FROM_NAME to .env file (laravel#5180) * use class name to be consistent with web middleware * Correct exception handler doc (laravel#5187) * Fix types consistency in database config (laravel#5191) * Revert "Apply fixes from StyleCI (laravel#5006)" This reverts commit 5017673. * Use file session driver again * Use file session driver again * Update CHANGELOG.md * Add missing full stop for some validation messages (laravel#5205) * Update laravel mix and sass loader (laravel#5203) * [7.x] Update cross-env and resolve-url-loader to the latest (laravel#5210) * [6.x] Update cross-env to the latest (laravel#5216) This is not a fix for the vulnerability. Just updating the dependency to the latest version. @see https://yarnpkg.com/package/cross-env Yes, I know that they recently released version 6.0 and in a short time 7.0. * Bump fzaninotto/faker version to support PHP 7.4 (laravel#5218) Bumping `fzaninotto/faker` version to support PHP 7.4, especially when running composer with `--prefer-lowest` flag. PRs related to version `^1.9.1`: * fzaninotto/Faker#1748 * fzaninotto/Faker#1843 Co-authored-by: Dries Vints <dries.vints@gmail.com> Co-authored-by: Taylor Otwell <taylor@laravel.com> Co-authored-by: Graham Campbell <GrahamCampbell@users.noreply.github.com> Co-authored-by: Tim MacDonald <tim.mac7@me.com> Co-authored-by: Gergő D. Nagy <hello@iamgergo.com> Co-authored-by: Chuck Rincón <chuckrincon@gmail.com> Co-authored-by: Christopher Lass <arubacao@users.noreply.github.com> Co-authored-by: Darren Craig <darrencraig@hotmail.com> Co-authored-by: Sjors Ottjes <sjorsottjes@gmail.com> Co-authored-by: Patrick Heppler <12952240+HepplerDotNet@users.noreply.github.com> Co-authored-by: James Merrix <james@appoly.co.uk> Co-authored-by: Roger Vilà <rogervila@me.com> Co-authored-by: Sangrak Choi <kars@kargn.as> Co-authored-by: Nuno Maduro <enunomaduro@gmail.com> Co-authored-by: Gert de Pagter <BackEndTea@users.noreply.github.com> Co-authored-by: Michael Chernyshev <chmv@allnetic.com> Co-authored-by: Mark van den Broek <mvdnbrk@gmail.com> Co-authored-by: byjml <byjml@users.noreply.github.com> Co-authored-by: Bert Heyman <bert.heyman@hotmail.com> Co-authored-by: Can Vural <can9119@gmail.com> Co-authored-by: M Stokoe <mstokoe0990@gmail.com> Co-authored-by: Andrew Minion <andrew.minion@luminfire.com> Co-authored-by: Anton Komarev <1849174+antonkomarev@users.noreply.github.com> Co-authored-by: Aimeos <aimeos@aimeos.org> Co-authored-by: Robert Korulczyk <robert@korulczyk.pl> Co-authored-by: Caíque de Castro Soares da Silva <castro.caique@gmail.com> Co-authored-by: Andrey Helldar <helldar@ai-rus.com> Co-authored-by: ARCANEDEV <arcanedev.maroc@gmail.com>
@driesvints |
@mitesh1409 Move them to the |
@driesvints So I believe if we want to make use of Model Factory feature in unit tests, then importing Thanks again :) |
@mitesh1409 you're entirely free what you want to do in your own app |
* Use in-memory DB for testing * Extend from PHPUnit test case for unit tests
Hey everyone. With this PR I want to propose a new dedicated Integration test directory and in-memory DB defaults to the default Laravel test suite. As someone who's worked quite a bit with different test suites over the past years I think I can say for myself I have quite some experience in this area and wanted to share some small things through this PR.
In-memory DB
The first thing I want to change with this PR is to provide new defaults for the database that's being used when running the test suite of the app. This is probably the very first thing I change for every single app I start working on. All of my tests run in memory against a sqlite database which is pretty fast I must say.
While it is true that the current default Laravel test suite doesn't requires a database to be set up, it is from my understanding that a lot of people do add these new defaults as soon as they need to start testing against a database. It's also true that an sqlite can't mimic a MySQL or Postgres database which is probably used in production but the speed of an in-memory database makes it much more suited for running tests. Therefor, I believe these are good defaults to have in the skeleton.
Integration Directory
From what I often read on issues and on Twitter I still see that there's a lot of confusion about what a Unit test is and what an Integration test is. A unit test is something you test in isolation, without booting the entire app. You, for example, test a single object or function without any outside dependencies and maybe mock some of those dependencies. These tests usually run really fast.
Integration tests, however, are tests which test the behavior of different components together. They aren't full cycle tests like End-to-End tests or Feature tests but they do require some additional setup (like a database for example). These tests tend to run a bit slower.
In the current skeleton setup you only have two directories: a Unit test directory and a Feature test directory. I think there's room for an Integration directory which actually copies the behavior of the current Unit test directory. Because you see, the current default Unit ExampleTest extends from the default TestCase. This isn't wanted for unit tests because it boots up the entire framework and thus slows down these tests which should actually run much faster (because they should be isolated).
What I changed is that I've added a dedicated Integration test directory which keeps the current behavior of extending from the Laravel TestCase class and can be used to test different components in your app which require the framework to be booted. The old Unit ExampleTest is updated to only extend the default PHPUnit Testcase so the framework isn't booted and you get the speed boost for these types of test. It's also ok that you don't have all of the Laravel test methods for the Unit tests because most of these methods are meant for Integration tests and provide no value in Unit tests.
I also wanted to make a clear different between what a Unit test and what an Integration test is inside the example tests. For the Unit tests I'm testing if the User model accepts a name attribute and that it can be fetched properly (most people also don't realize you can perfectly Unit test Eloquent models as long as you don't do DB operations). For the Integration test I've added an example which actually makes use of the newly in-memory database defaults and I seed three users and fetch them with the User model.
Conclusion
What I'm mainly trying to achieve with these changes is to educate people better about testing in general and what the different types of tests are. And I'm hoping this saves a lot of people some time when they start on a new Laravel app (even though the changes are minimal).
I'm sorry for writing such a large description. I'm usually against these myself hehe. But I wanted to be thorough enough to explain the reasoning behind it🙂
Let me know what you think.
Update: Taking from the feedback here I've made changes to this PR. More info here: #5169 (comment)