Problem/Motivation
BrowserTestBase creates a fresh Drupal installation for each test. This takes a considerable part of test execution time and has a negative impact on developer experience.
Another performance problem is enabling modules that a test depends on. It is quite expensive operation and in some cases it may take even more time than installing Drupal.
Steps to reproduce
Just run phpunit tests for core or contrib/custom.It so looong.
Proposed resolution
The possible solution could be dumping a database into SQL file just before the test case get executed. So that on the next test run Drupal can be installed using predefined SQL dump which is much faster the normal installation process.
How to test
- Run any test with a lot of test functions. Record time.
- Install patch from MR
- Update your phpunit.xml (needs to add BROWSERTEST_CACHE_DB and BROWSERTEST_CACHE_DIR)
- Run previous test. Record time. It should be faster
Remaining tasks
- Needs to add sqlite support
- Needs manual testing by community
- Add BROWSERTEST_CACHE_DB to phpunit.xml.dist as per #55-56
User interface changes
No
Introduced terminology
No
API changes
No
Data model changes
No
| Comment | File | Size | Author |
|---|---|---|---|
| #60 | 2900208-60.patch | 6.23 KB | kaustavbagchi |
| #59 | 2900208-59.patch | 6.24 KB | kaustavbagchi |
| #48 | 2900208-48.patch | 6.17 KB | owenbush |
| #47 | interdiff-33-47.txt | 737 bytes | owenbush |
| #39 | DemoTest.php_.test | 345 bytes | tyler36 |
Issue fork drupal-2900208
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
chi commentedThe performance gain of caching a database varies. While for some tests the total execution time remains almost the same on other tests it can be reduced 5 and more times.
Comment #3
chi commentedComment #5
chi commentedRerolled the patch to 8.x-5.x.
Comment #6
Anonymous (not verified) commentedLooks awesome! 💪
Comment #7
dawehnerComment #8
borisson_I really love this as an idea, we're running daily tests for the search ecosystem on travis and I'm constantly running into the travis time limit with facets: https://travis-ci.org/mkalkbrenner/search_api_integration_tests/builds/3...
@var string?Should be one line.
Let's do,
array_unique(sort($modules));to make sure that when a class extends the another one and for some reason the same module is declared twice (because the base class changed or something like that).Should we worry about filename length here? This can lead to really long filenames. Some of the contributed modules that extend already existing tests can have easily 8-9 modules.
I'm not sure this is something I like. Adding an exec isn't great, I also don't think we should add the
sedhere, that would make windows systems not able to use this, I think?I'm not sure that's the best solution.
Same here.
Comment #9
dawehnerIt seems to be that the experiment @fabianx did a while ago in #2759197: [D7] Improve WebTestCase performance by 50% did not required the
mysqldumpcommand. Have you seen this issue? Do you think we could incorporate that strategy?Comment #10
chi commented@dawehner, I just checked it. They do not dump data to filesystem at all, but simply copy tables within the same database. I think it similar to what SimpleTest Clone does.
At this point I am not sure which approach is better.
Comment #11
dawehner@Chi
Your approach seems to for example not support pgsql, am I right?
Comment #12
chi commented@dawehner right, but technically it is possible by supplying different shell commands for each database type like
drush sql-dumpdoes.The approach with copying tables is cross-platform and definitely more reliable from this point.
Comment #13
borisson_In that case, I think we should close this issue and try to get #2747075: [meta] Improve WebTestCase / BrowserTestBase performance by 50% going again. At least a reroll on that issue should help. While it might not be as big an improvement as this issue, it still is an improvement and it will work for all databases.
Comment #15
jibran#2747075: [meta] Improve WebTestCase / BrowserTestBase performance by 50% is meta so I don't think we should close this one. This can be its child also the patch in #2747075 is for WTB and this is for BTB so this is more up to date.
Comment #16
fabianx commentedI think this should just re-roll the latest patch from the meta issue - it was a functional port.
Comment #17
lokapujyaThis issue is awesome!
Comment #19
thtas commentedI'm really liking this idea too. Functional tests are so useful, but it's such a pain how long they are taking.
Maybe instead of the exec approach, we could be doing something similar to how Drupal\FunctionalTests\Update\UpdatePathTestBase works by loading the DB from output generated from the dump script at scripts/dump-database-d8-mysql.php
Comment #20
mile23Since it hasn't been mentioned here: Just a heads up that #2973227: Move UI helper functions from BTB into the traits was committed to support https://gitlab.com/weitzman/drupal-test-traits
That's a third-party testing framework that allows you do to functional tests against an existing site.
Comment #22
jibranCreated a reroll from scratch. Reverted
DrupalKernelchange. Addressed #8.1,2,3.Comment #23
jhedstromThis might be an over-engineering suggestion, but if this were instead implemented as a plugin, then in theory non-core DB implementations could also utilize this, and the code would be a bit cleaner as all db-specific loading/dumping logic would be encapsulated in the plugins...
Comment #24
jibranI agree the patch is little rough around the edges but before proceeding with this we should have some kind of consensus from contributor or approval from maintainers.
Comment #25
andrewbelcher commentedWould really love this patch! However, I hit the following error. The test has quite a lot of dependencies and I'm running in DDev, not sure if the either (or both) of those are triggering the issue:
Comment #27
mglamanFor something in contrib this would be a huge filename and probably crash some filesystems, maybe Windows which has problems with large file paths.
Perhaps a hash of the modules array is more appropriate.
Comment #28
hestenetThe topic of making testing significantly faster (and thus more affordable) has certainly become top of mind again.
I would love to help bring attention to this effort using DA channels, if that would be helpful.
It's also rather late in the game to be suggesting alternative approaches, but another suggestion from the world of containerization could be to scaffold testing using a base, preinstalled Drupal environment, build new instances on top of that base for each test, resetting to the base each time to ensure a clean environment.
Comment #31
ruuds commentedThe patch of #22 cut down my testing time in half (35 to 16 minutes)!
Although it's not the cleanest solution as mentioned before, i've made some changes:
- The dumpFile's filename is now a md5 hash of the imploded module list
- Tests using the public and private filesystems failed
- mysqldump / mysql now takes the database hostname in account.
Comment #32
ruuds commentedAllright.. The correct patch file here.
Comment #33
adityasingh commentedFixed coding standards error.
Comment #34
oliverpolden commentedPatch in #33 worked for me. Brought test time down from 12.7 minutes to 56 seconds in my case.
Comment #35
alexpottHash salt is duplicated and could be set to
Crypt::randomBytesBase64(55)which i think is more correct.Instead of doing this we can use the database dump command.
Then to install from a dump we can do:
This also needs to include the default theme and profile.
Comment #36
alexpottAlso I don't think this is useful for 99% of core browser tests - we use the testing profile and only ever install a few modules. It won't make as much difference now the install is much quicker. But this would be super super useful functionality for real projects with their own dump or install profile.
We do need test coverage to prove things actually work.
Comment #37
chi commentedThere is another much more simple approach to speed up tests which is in-memory SQLite database.
<env name="SIMPLETEST_DB" value="sqlite://localhost//dev/shm/test.sqlite"/>Comment #38
alexpott@Chi that's good for running single core tests but if you testing a site build with a large install profile (lots of modules and config) using a dump really really helps. Also I think in-memory sqlite dbs suffer when you do testing via webdriver (or anything that uses a real browser) because of the multiple requests. Like even create image style derivatives can cause a sqlite memory db to be really slow compared to another db type.
Comment #39
tyler36 commentedWhat level of speed improvment is being aimed for here? Is everyone testing against a common test here?
Let's establish a baseline:
Note: DemoTest.php is the simpliest / fastest functional test I could think of. I understand that functional test usually involve enabling modules, creating nodes, manipulating users. With a common baseline, it's easier to establish setups, settings, pain-points, that can be targeted or improved.
Setup:
- Copy
DemoTest.php_.testinto<drupal_root>/modules/custom/demo/tests/src/Functional- Rename
DemoTest.php_.test->DemoTest.php(I can't upload a PHP file to this site)- Run PHPunit, tartgeting test.
Clean drupal install results:
- Apply
2900208-33.patch- Run PHPunit, tartgeting test.
Clean drupal install results:
- No noticable difference but then the test isn't really doing anything other then starting up.
Comment #40
chi commentedThere is a simple benchmark in comment #3.
Comment #41
alexpott@Chi the Drupal installer has had significant performance improvements since then so any benchmark from 2017 needs to be re-run. As I've pointed out the real utility for this is on real sites where installing your real site can take quite sometime because you're dealing with way more modules and config and sometimes content. I think we should provide a test trait along the lines of https://gist.github.com/alexpott/8cd82bc75b88c01489d837cf8432451b that we document or even consider automatically using when the profile is not one of the core testing profiles.
Comment #43
guypaddock commentedPerhaps the Umami test would be a better common baseline:
https://api.drupal.org/api/drupal/core%21profiles%21demo_umami%21tests%2...
Comment #45
wim leers@joevagyok has been using this patch at the European Commission for years, and it improves test performance ~4x!
I think this issue may have been underestimating the indirect benefits for concrete projects. Even if it's not 4x but just 1.5x faster … then it would be a huge productivity boost for many Drupal projects.
@joevagyok will push this forward tomorrow :)
Comment #47
owenbush commentedWe are testing out this patch, to help with our 26min+ functional test runs, and for the most part it seems to be working as expected. However, one of our tests checks the
X-Drupal-Cache-Tagsheader and we noticed that on initial runs (no DB dump yet) the tests pass, but on subsequent runs (restoring from the DB dumb) the tests fail, and that's because theX-Drupal-Cache-Tagsheader is not present.This seems to be due to the fact that
FinishResponseSubscriber::debugCacheabilityHeadersis false.In tests which run
installDrupalFromProfile()the cacheability headers are set by calling$this->setContainerParameter('http.response.debug_cacheability_headers', TRUE);from withininitSettings(). That never gets called when tests runinstallDrupalFromDump()This patch addresses that problem, with an interdiff provided too.
Comment #48
owenbush commentedRe-rolled patch #47 to apply to 9.4.6
Comment #50
blazey commentedHey, we had a similar case but limited in scope. Our project has only kernel tests and uses sqlite, so I wrote a module that handles that. Feel free to test it out / benchmark in your projects if that fits your case: https://www.drupal.org/project/sqlite_test_cache
Comment #51
fjgarlin commentedIs this issue still in "Needs work" or "Needs review"? I see a new patch was added at #48, just not sure if it addresses all of #35 feedback.
Comment #53
ivnishPatch #48 works with Drupal 9.5
Run tests of my module without this patch: Time: 01:27.310
Run tests of my module with this patch: Time: 00:36.736
tyler36, you need to add BROWSERTEST_CACHE_DB=1 before phpunit exec. In my case this is
fin exec BROWSERTEST_CACHE_DB=1 vendor/bin/phpunit --testsuite=my_moduleComment #54
tyler36 commented@ivnish Thank you for pointing out the
BROWSERTEST_CACHE_DBrequirement.Tested #48 on Drupal 10.0.9 one existing project with mixture of unit, kernel and functional tests (32 tests, 280 assertions).
Time went from 02:28 => 1:13. Multiple runs averaged about 50 second saved.
Comment #55
tyler36 commentedI think the environmental variable should be added to
core/phpunit.xml.dist. This allows it to be the "default" setting.Comment #56
fjgarlin commented#55 is a good suggestion. I'd add it to the list of changes requested in #35
Comment #57
ivnishI have a lot of bugs with DB cache. For example "unknown schema" and "route not found". Needs to remove old dump
Comment #59
kaustavbagchi commentedThis is an updated version of 2900208-48.patch which is now compatible with Drupal 10.1.x version.
Comment #60
kaustavbagchi commentedThis is an updated version of 2900208-59.patch with whitespaces removed, compatible with Drupal 10.1.x version.
Comment #61
_utsavsharma commentedFixed failures in #60.
Comment #62
ivnishI manually compared patch #48 and patch #59 and #60. Patches #59 and #60 are garbage patches without any changes. Only code style was broken
Patch #61 added one new change
Comment #63
catchIt would be useful to see if this makes a difference on gitlab ci. We'd need an MR, with the environment variable shortcutted (or set in the gitlab YAML).
Comment #64
smustgrave commentedgitlab would make sense. and #60 would be good to explain why you added a new change.
Comment #67
dave reidI found an issue using the latest version on 10.2 where
$connection_info['default']['driver']is nowDrupal\mysql\Driver\Database\mysqlinstead ofmysql. Opened an official MR for better visibility and GitLab CI testing.Comment #68
dave reidComment #69
guypaddock commentedSo far, I've been able to fix the concurrency issues and improve performance by reworking the methods as follows:
An interdiff (against #48) is attached for the changes above, but I was curious about feedback on this approach before I apply them to the MR.
Comment #70
wim leersIf a full install + module installs are no longer needed, but just a
drush site:install --existing-config, then that would already speed up things considerably.Which means that #3406929: Configuration being imported by the ConfigImporter sometimes has stale original data (which just landed) should also help with this.
Comment #71
lexbritvin commentedThe provided patches don't consider install profile and default theme.
Here is a small enhancement to the patch.
Comment #73
ivnishComment #74
ivnishLet's use MR, not patches
Comment #75
ivnish@GuyPaddock can you apped MR with your changes #69?
Comment #76
ivnishComment #77
jonathan1055 commentedThere is a call-out on slack asking for help to test this
https://drupal.slack.com/archives/C223PR743/p1723469165431189
Can someone update the issue summary here to explain how you would like this tested, how to run it, what info you want, with & without the change. Thanks.
Comment #78
ivnishI will update issue summary
Comment #79
andypostbtw Sqlite just needs to copy DB-file after install
Comment #80
andypostComment #81
ivnishComment #82
ivnishComment #83
ivnishComment #84
catchWould be good to see how this affects test run times once the MR is green but also we should check it against the times on #3416522: Add the ability to install multiple modules and only do a single container rebuild to ModuleInstaller which significantly improves performance of the installer.
Comment #85
smustgrave commentedMR actually appears to be red
Comment #86
jonathan1055 commentedI have rebased the MR, it was 110 commits behind.
The MR is red because of a couple of PHPStan failures
These are in the new functions dumpDatabase() and restoreDatabase(), but the critical bit I think is
in context of class Drupal\TestSite\Commands\TestSiteInstallCommandas that uses this trait, see11.x/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php?ref_type=heads#L27-31
There must be several ways to the solve the phpstan error here. Not sure which is preferred. It was attempted then reverted
https://git.drupalcode.org/project/drupal/-/merge_requests/5744/diffs?co...
Comment #87
jonathan1055 commentedI moved the definition of
dumpFilefrom BrowserTestBase to FunctionalTestSetupTrait. Given that BrowsertestBase uses that trait this should still work, and it solves the PHPstan error.Comment #88
jonathan1055 commentedComment #89
jonathan1055 commentedThe MR pipeline now runs green :-)
I am testing this locally, but I get
and this halts the tests and gives "This test did not perform any assertions"
What's the bet way around this? There are multipe ways to do it, maybe we want to adust the two new mysql commands to use a file for the credentials?
Comment #90
feyp commented> [Warning] Using a password on the command line interface can be insecure.Last time I looked, drush created a temporary file using
tmpfile()with the login data and passed that to mysqdump / mysql via--defaults-extra-file. Given that the temporary file is deleted by PHP at the end of the execution of the process and we're only using this during testing and not in production, this might be sufficiently secure, at least more secure than passing this on the command line or using environment variables (on the other hand, I guess we're already using env vars during testing anyway, so nothing gained over using env vars?). We could also re-check drush sql commands before we start implementing this, maybe there is now another solution that is even better?Comment #91
jonathan1055 commented@feyp I don't think there is anything to do with drush here. This is a core issue, and drush is not used to execute these commands.
I was not asking from the point of security (others will have an opinion on that). I just wanted to know how I can test this locally. Some other devs have reported being able to run this locally. Maybe the warning can be suppressed so that the tests continue?
Comment #92
feyp commentedYes, I know that, of course. I was referring to the implementation of drush sql commands, which also call mysqldump, as a reference implementation on how to suppress this warning in code and was suggesting to do something similar here so that you don't run into this warning when running the tests locally.
Comment #93
andypostThat's because CI images using specific config and predefined users https://git.drupalcode.org/project/drupalci_environments/-/blob/dev/db/m...
Comment #94
jonathan1055 commentedI have this working locally now using the method @feyp suggested - writing the user and password to a temporary file then using
--defaults-extra-file=$user_credentials_fileinstead of the-u$user -p$passarguments. If this is an acceptable solution I will tidy it up and push it here for comments. But if this is a non-starter I will discard it.Comment #95
smustgrave commentedDumb question but good way to test this one?
Comment #96
catchI just kicked off a pipeline to check if it's still green and try to see what the timings look like.
Another way would be to run a test that is likely to benefit from the change locally before/after and record the times - although probably need to do middle of three to account for variations.
Comment #97
smustgrave commentedSeems to have some test failures
But left some comments/questions on the MR.
Comment #98
quietone commentedChanging to the DX special tag defined on Issue tags -- special tags.
Comment #99
ivnishFor Drupal 11 needs to remove
because path.temporary is deprecated
Comment #100
ivnishFolks, I'm trying to add sqlite support. How to import sqlite dump?
exec("sqlite3 $db-$this->databasePrefix < $this->dumpFile");doesn't work because NOCASE_UTF8 not registered (old drupal sqlite issue)