-
Notifications
You must be signed in to change notification settings - Fork 11.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[8.x] Adds new RefreshDatabaseLazily
testing trait
#38861
[8.x] Adds new RefreshDatabaseLazily
testing trait
#38861
Conversation
Thanks. What are the before / after benchmarks on some real world scenarios? |
Obviously greatly depends on the application; how many non-database tests vs database tests. In an application with ~400 tests, where around 90% of those tests use the database, I went from 24s to 21s. |
Isn't the concern around the emitting of the event on every query called during the application's lifecycle and not directly with the test performance? If an application performed 100 database queries on a request (arguably poor design but you never know!), would the Would it be safer to only emit the event when in the testing environment to begin with? |
What happens if a test only runs SELECT queries (no INSERT/UPDATE/DELETE queries in this test) ? Will the database still be refreshed ? |
@taylorotwell here you can see another application, this one using MySQL rather than SQLite in memory and the saving is pretty drastic... |
@windu2b |
@timrspratt thanks for the feedback. I've done some testing on this, and with no changes, the event call takes between 0 and 0.1 milliseconds. In honesty, I don't know enough about lambda costs to know how much that would impact costs. I would certainly assume that there would be much larger impacts on costs, such as multiple or inefficient database queries as you pointed out. Would be good to get some opinions on this for sure. |
Creating a Could there be a |
@garygreen ideally we'd link into the connection booting, but that happens regardless of whether a query will be run, rendering it a little useless. Any ideas? 🤔 I'll take another look tomorrow to see if we can reduce the event count whilst keeping it clean. |
I haven't checked, but that sounds a bit strange if it pre-emptively connects. Briefly looking at the code it creates the PDO object on demand when needed, and so should only create the connection when it's needed... |
@garygreen found the cause.
The cause is the So whilst Laravel wouldn't attempt a DB connection early, tests always will. The I've switched out event calling for another solution, which I think provides the best of both worlds. |
But I think it's useless to refresh the database, if there are only |
@windu2b in an ideal world, yes. However, if a developer has used a select statement and no database exists, it will throw an exception, which would break a previously passing test. |
Isn't the root problem that you are adding that trait to tests that don't use the db? Can't you just not do that? |
@GrahamCampbell up to this point, that's exactly what I have done in my applications. However, it's annoying to have to keep track of this and muddies up the test cases. In my opinion (and I dare say in the opinion of a large percentage of the community), this removes complexity and makes it easier to maintain a fast test suite. If we start saying "can't you just not do that" about things, much of Laravel wouldn't exist. I really appreciate your insights and contributions by the way; just stating my case. |
RefreshDatabaseLazily
testing trait and QueryExecuting
eventRefreshDatabaseLazily
testing trait
@GrahamCampbell I think this will help reduce the cognitive load for newcomers to testing. In my opinion, this is the kind of change that will reduce friction and help more and more people have a good experience with testing 👍 |
I am just trying to decide if encouraging people to jam all the traits into their tests should be considered a bad practice. We don't encourage people to install every package on packagist in case they need to use them. What's so wrong with having to say "run this test with db migrations"? It'll make people think, "do I really need to hit the db for this test?". Lots of people have really slow test suites because they are writing "unit" tests that unnecessarily boot the framework and do all kinds of stuff. And, yes, I'm playing Devil's advocate here. |
I've personally only added the So benefit of this PR will come at a per-test level where it only refreshes when it knows a database query is being run. Still seems like a win if it can be done in a way that doesn't impact main app. The new callbacks are ok, but I would prefer to see a general one time It would be ideal to listen for when database was connected and then assume it's needed. If something in Laravel is connecting too pre-emptively then that should be addressed? |
@garygreen how would you handle parallel testing? Laravel has to make an early connection in order to build the required databases in preparation for running across processes. Addressing your package concern, any package can already spam DB::listen and slow down every query on the other side, no? 🧐 |
That's the main purpose of this PR, to hook into some mechanism that defers refreshing the DB? Curious to know why does Laravel have to make an "early connection" ? That may well be the case at the moment so wonder if that process can be deferred - |
@lukeraymonddowning |
@lukeraymonddowning were the benchmarks you shared on this feature on a real-world application or specifically crafted for this PR? In my own applications I don't think I would see any benefit from this trait because every feature I test that hits controllers in my application is going to authenticate a user which is going to run a database query. It's actually probably pretty rare for me to write a feature test that does not interact with the database at all. |
That's true for my apps too. Even a unit test that involves a model uses the DB in most cases. Anyways it would be cool to have the event in the framework but the trait in a sperate package aka user-land. |
@taylorotwell this was a real world application that we are currently in the process of building. No tweaks were made to the code other than switching out A more detailed breakdown of the tests:
Examples of tests in the project that don't touch the DB include:
Can't be super specific on the nature of the tests due to NDAs on the project, but they're pretty typical tests. |
@lukeraymonddowning and this application is the one where you saw improvement from 24 seconds to 21 seconds? |
That's the one 👍 the second example is quite light on the database as it stores the majority of its state in an external API. |
Renamed to |
excited to release this |
* Adds a new `LazyRefreshDatabase` testing trait and a `QueryExecuting` database event. * Refactor * Refactor * Event test * Naming change * Switches out event for an array of callbacks * Refactor * Refactor * Refactor * Refactor * Refactor * formatting * fix doc block Co-authored-by: Taylor Otwell <[email protected]>
Hi guys,
Hope you're well! Lemme break down this PR.
Background
When testing, we often add
RefreshDatabase
to our tests to migrate the database between tests. This is super useful but comes with a speed penalty. Because it is frustrating to have to add theRefreshDatabase
trait to everyTestCase
that uses it, developers often just drop it on the baseTestCase
in PhpUnit or use something likeuses(RefreshDatabase::class)->in('Feature');
in Pest PHP. This means that tests that have no need to interact with the database will still run migrations.To quote Caleb Porzio: 'No Bueno.'
Solution
This PR adds a new drop-in replacement for
RefreshDatabase
:RefreshDatabaseLazily
. By utilising a newQueryExecuting
event bundled in with this PR, the test is able to wait until the database is actually required before running the migrations. This means that if a test has no need for the database, migrations will never be run.The impact of this PR is that you can drop this trait on your base
TestCase
with 0 consequences. The newQueryExecuting
event also allows for users with more complex database requirements to implement similar functionality in their test setup manually.Why not just alter
RefreshDatabase
directly?It is entirely possible that developers make use of Laravel's migrations, but then interact with the database through raw PDO statements. This would lead to the
QueryExecuting
event never being fired, and the migrations never being run.As such, allowing developers to opt in to this feature if their application solely makes use of Laravel's DB tools allows for a non-breaking change. I feel the majority of devs will be able to start using this trait immediately, but better safe than sorry.
As always, I want to say a huge thank you for all of the hard work you continue to put into maintaining and building upon the Laravel framework. I am eternally grateful.
Kind Regards,
Luke
Edit: in order to alleviate concerns about performance caused by the QueryExecuting event, I have removed it in favour of a more simplistic and streamlined callback array. This allows for the same functionality without the overhead.