Repeat: How many tests will actually fail if setUp() is only executed once for a test case?
This patch cuts down the total time for the full Drupal core test suite to 9 minutes. (from currently 20+ minutes)
API changes
- The current
setUp()andtearDown()methods are replaced withsetUpBeforeClass()andtearDownAfterClass():
- function setUp() { - parent::setUp(array('taxonomy', 'node')); + function setUpBeforeClass() { + parent::setUpBeforeClass(array('taxonomy', 'node')); setUpBeforeClass()andtearDownAfterClass()are only invoked once for each test class.setUp()andtearDown()are however retained, and invoked before and after each test method in a class.- All test methods in a class share the same test environment.
- Test methods are executed in the order they are defined.
- Test methods need to ensure that they are either executed in the right order, or revert any special conditions that they have changed, so following test methods being executed afterwards still work.
This especially applies to configuration or context that is set up once for test methods and is shared between them (so called "fixtures").
Example for stuff that breaks:
class WhateverTest extends WebTestBase { function setUpBeforeClass() { parent::setUpBeforeClass(); $this->admin_user = $this->drupalCreateUser(); // If any test method logs the user out, following tests will break. // Move this into setUp(), so admin_user is logged in for each test method. $this->drupalLogin($this->admin_user); $this->node = $this->drupalCreateNode(); } function testMaintenanceAccess() { // Maintenance mode will be enabled for all following test methods, which // obviously makes most tests fail. // Reset or delete the variable to its original/default value at the end of this test. variable_set('maintenance_mode', 1); } function testDelete() { // This deletes the shared node and thus breaks testResave(). // Move/relocate this test method after testResave() - or alternatively, // take the appropriate measures to restore the expected data at the end of // this test (which either means to update $this->node with a new one, or, // in case the test requires it, even with the identical ID $this->node->nid). node_delete($this->node->nid); $this->assertFalse(node_load($this->node->nid), FALSE); } function testResave() { $resaved_node = node_load($this->node->nid); node_save($resaved_node); $this->assertIdentical($this->node->nid, $resaved_node->nid); } }
Help to get this done
@sun will not be able to champion all of the required test changes on his own. But there is a sandbox, so you can help! :)
How to help:
- Ask for git write access to the Platform sandbox, if you haven't already. IRC is fastest; but a comment here works, too.
- Setup the Platform sandbox for your local D8 git repo/clone.
- Checkout @sun's branch into a branch that's specific to you:
git checkout -b test-once-1411074-[username] platform/test-once-1411074-sun git push -u platform test-once-1411074-[username]This means you're using @sun's branch as the tip to work off from. @sun will review your changes and merge them into the main branch, if appropriate.
- To create patches for the testbot, diff against the -base branch:
git diff platform/test-once-base test-once-1411074-[username] - To merge new changes from a test-once branch from another user:
git merge --no-ff platform/test-once-1411074-[otheruser]The
--no-ffoption is key here. - Operations you are not allowed to do:
git push platformwithout specifying a branch name: This would push all of your local branches into the platform sandbox.git push platform [8.x:]test-once-base: Only @sun updates the base branch when needed.git push platform test-once-1411074-sun: You only push to the branch that has your username.git rebase: Rewrites history and makes your branch incompatible / unmergeable.git pull 8.x: Merges in all latest 8.x code into your test-once branch, but all work is based on the -base branch.git merge 8.x: Ditto.
| Comment | File | Size | Author |
|---|---|---|---|
| #31 | 1411074_poc_do_not_test.patch | 7.34 KB | mile23 |
| #25 | test.once_.25.patch | 10.97 KB | sun |
| #22 | test.once_.22.patch | 14.85 KB | sun |
| #10 | test.once_.10.patch | 211.78 KB | sun |
| #8 | test.once_.8.patch | 201.84 KB | sun |
Issue fork drupal-1411074
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
moshe weitzman commentedPHPUnit has two methods, setUp() and setUpBeforeClass(). teardown has similar structure. So, a test author can choose to implement either. As a general rule, I agree that one drupal install per class is good.
Nice work so far!
Comment #3
xjm#2 sounds like an excellent pattern to me.
Comment #4
sunThis makes soooo much sense: http://www.phpunit.de/manual/3.7/en/fixtures.html :-/
So. Introducing setUpBeforeClass() and tearDownAfterClass(). A major leap towards PHPUnit migration.
Awesomeness! :)
Comment #6
sunFixed fixtures.
Comment #8
sunFixed more fixtures.
Comment #10
sunAnd more.
Comment #11.0
sunUpdated issue summary.
Comment #11.1
sunUpdated issue summary.
Comment #12
carlescliment commentedTake care. The abuse of this method can drive you to coupling between tests and lack of isolation.
Comment #13
btmash commentedHmm...I both like and the idea and am a little worried by it. The idea that a test class now only has to get setup once for all tests certainly seems pretty awesome if only for the speed bump. But at the same time (and maybe I am mistaken so someone could knock some sense into me), I can't help but think of a class now being one large continuous test. What @carlescliment said in isolating the issue (instead of reading through the setup and test function, you would now be reading through the setup + all test functions prior to and including the test function to debug what the test issue could be) does worry me a bit.
Comment #14
sunThe downside of shared fixtures are well known. Please read the PHPUnit docs I linked to in #4.
However, this is nothing new in general. If you're afraid of fixtures, then don't use them, and instead create your users/nodes/fields/whatever from scratch for every test method. After all, that's what the setUp() method is for.
The real difference takes effect for the 99.9% of things your test actually does not test, but which you need nevertheless in order to execute the test in the first place. Alas, the extremely optimized testbots (which run with a database on /tmpfs, among many other performance optimizations) spend more than 10 minutes with setting up a new environment for every test method, even though none of our test methods needs a completely new.
Lastly, the shared fixtures between test methods is not concerning at all, given that we're not even able to properly isolate the test from the test runner process. We're currently bumping from one critical bug into the next, and the duct-taping costs plenty of wasted man hours every single week to keep the system running. That's why we want to move away from our homegrown Simpletest and use one of the existing standard testing frameworks in the PHP world instead. This change here is a huge leap towards that.
Comment #15
btmash commented@sun, Thanks for the clarification. I was reading through it and the line:
Was what brought up part of the doubt. But if we're not able to isolate the tests anyways (and this code moves us towards phpunit which hopefully does help with our test isolation issues and overall will do much more good anyways), then awesome :) I'll make some time and try to post some updates in your sandbox towards this.
Comment #16
sunLacking progress here, let me propose a different plan of attack:
WebRealTest[or whatever name].I'm fully aware that this will introduce a "second" (duplicate) layer of how people can write tests.
However, I'd rather have this progress, instead of no progress at all.
Comment #17
lucascaro commented@sun what about installing drupal normally in setUpBeforeClass and saving a database snapshot that we can restore on setUp? would that be a good compromise between speed and keeping things working?
Comment #18
moshe weitzman commentedwe have discussed db restore in the past and Upal even implements that approach. But we are now using the Testing profile a lot more and we need to keep stripping that down so that it does not do menu build and node install and so on. I think we can speed up Testing and use more UnitTestCase such that we don't have to change to DB restore method.
Comment #19
Sylvain Lecoy commentedIs there any issues regarding PHPUnit migration ? A [meta] issue or something that records every moves to PHPUnit ?
Comment #20
sun@Sylvain Lecoy: There's no official meta issue yet; only #1567500: [meta] Pave the way to replace the testing framework with PHPUnit and possibly rewrite Simpletest. However, that's mainly a temporary dumping ground for collecting relevant input and information. It's definitely missing the latest and greatest results of discussions I had with various people. I will try to create a new and proper meta issue as soon as possible. Meanwhile, all related and forward-thinking issues are tagged with Testing system.
Comment #21
Sylvain Lecoy commentedOk so I created this [meta] issue: #1801176: Deploy a PHPUnit system with a bottom-up approach
I also contacted Moshe Weitzman to ask him if there is any work in progress that I can take over. I am so pissed-off by SimpleTest today that I am making my personal battle to work on replacing it by PHPUnit.
Comment #21.0
Sylvain Lecoy commentedAdded API changes.
Comment #22
sunTrying once more, with a new approach that hopefully works better:
→ Introduced new
SingleEnvInterfaceto explicitly opt-in tosetUpBeforeClass()+tearDownAfterClass().FWIW, in order to not run into total indentation hell, I also had to adjust the execution flow in
TestBase::run(). Will likely split that out into an own issue, so that this is more reviewable.Comment #23
sunSaid spin-off: #2201783: Simplify execution logic in TestBase::run()
Comment #25
sun#2201783: Simplify execution logic in TestBase::run() has landed + also fixed the undefined
$classvariable test failures of this patch.So this is much easier to review now — what do you think of the approach taken? In essence:
setUpBeforeClass()is not invoked for tests.SingleEnvInterface, then it has to implementsetUpBeforeClass()+tearDownAfterClass().setUpBeforeClass()once before all tests, then proceeds as usual, but Drupal is only installed once for all test methods.WebTestBase::setUpBeforeClass()performs the Drupal installation, andWebTestBase::setUp()turns into a no-op.Comment #26
tstoecklerNow that the test execution process has been revamped a bit I wonder whether we can't get away without the complexity that is added by the conditional logic in this patch.
I.e. couldn't we go back to something like #10 but without converting the whole slew of test classes:
- Make TestBase unconditionally call setUpBeforeClass() and tearDownAfterClass() at the appropriate places.
- Rename WebTestBase::setUp()/tearDown() to setUpBeforeClass() and tearDownAfterClass(). The same for DrupalUnitTestBase and UnitTestBase.
- Add empty setUp() and tearDown() methods to those classes.
- Convert one or two test classes to setUpBeforeClass()/tearDownAfterClass().
??
I may be missing something.
If we stick to this approach we should rename SingleEnvironmentInterface. In general, I'm not sure on the terminology. If we stick to environment the two methods really should be setUpEnvironment() and tearDownEnvironment().
Comment #27
sunThe old approach requires to convert all tests to the revised test execution flow. That is, because many tests expect that every test method operates in a fresh installation. In turn, all tests would have to be adjusted.
To avoid that, the idea is to allow tests to explicitly opt-in to the different environment setup and execution flow.
Renaming the
setUpBeforeClass()andtearDownAfterClass()methods is definitely a no-go, because the names are derived from PHPUnit and meant to be consistent with it. After all, our long-term goal still is to get rid of Simpletest.Comment #28
tstoecklerHmm... OK, that makes sense then. But the ultimate goal is still to not have this interface and to have all test classes implement this, right? (Even if we might not reach that goal for D8) And for e.g. new tests that get added they should also implement this right off the bat.
Regarding the method names, I didn't know PHPUnit supported that, that's cool. I still think the interface should be renamed to "SingleEnvironmentInterface" and it should have at least one or two more sentences of additional information:
* That implementing the interface leads to a performance increase.
* That all new test classes should implement this so that we can eventually remove the interface (given that that is true)
* That this is compliant with what PHPUnit does.
One nitpick:
This could use is_subclass_of() I think.
As the logic is a little bit involved this could use a couple more eyes, although I couldn't find any flaws.
Comment #29
mile23I'm not sure why this can't simply be another base class, where setUp() is marked final, and setUpOnce() gets called on subclasses.
Comment #30
sunI already investigated the idea of a separate base class (multiple times even), but the major problem with that is that we'd have to duplicate multiple base classes, because
TestBase::run()contains the actual test class/method dispatching code, whereasWebTestBaseis the subclass that primarily needs the setup-once behavior. In turn, we'd have to duplicate both (or move all code into traits), but I fear that such an approach would make the overall code much more complicated to maintain.Comment #31
mile23How about adding a flag in getInfo()? This will have the advantage of being easy to backport, too.
Comment #32
webchickI admit I have not read this issue, but I did cmd+F for "upstream" at least and didn't find anything mentioned.
I'd really rather not go down the same road we did with SimpleTest of essentially "forking" the library and making it full of Drupalisms, some of them actually useful (like this one sounds). Can we file an upstream patch for this feature?
Comment #33
moshe weitzman commented@webchick setUpBeforeClass() and tearDownAfterClass() are already part of phpunit. This issue brings us *closer* to PHPUnit, not farther.
Comment #34
webchickRock! :) Carry on, then!
Comment #35
mile23Pretty sure that boat has sailed, unless the goal is to re-implement SimpleTest based on the real framework, in which case this issue needs to be waaaay postponed. https://packagist.org/packages/simpletest/simpletest
How to make Drupal do modern, performant functional testing using PHPUnit as a framework:
1) Decide that the whole thing can be dependency injected by saying
$app = new DrupalKernel('test', $etc, $etc);2) See: http://symfony.com/doc/current/book/testing.html#your-first-functional-test
Seems obvious to me this is not a goal for Drupal 8 or it would have happened a while back. Like, first. :-)
But as for this issue: Which do people prefer? Add an interface, or make a flag in getInfo()?
Comment #36
jhedstromPatch no longer applies, but it is probably more work than a simple re-roll at this point since much has changed in the past year.
Comment #43
avpadernoComment #52
mondrakeComment #54
joachim commentedThis should be resurrected - but should be split into separate issues for Kernel / Browser / JS tests.
Comment #55
mile23Comment #56
chi commentedIt feels if we had fixed this issue in 2012 the Drupal Association could save tens of thousands of dollars on CI environments.
Comment #57
joachim commented(Posted on wrong issue)
Comment #58
mgiffordHow often are we running these tests? Seems like a good investment of time to cut the execution time in half, even if it's a matter of allowing us to know earlier that the patch works.
Comment #59
mgiffordIt has been over a year since someone looked at it. If @chi's perception of this is right, there are thousands more in savings that have been lost (let alone the environmental impact of running millions of processes on servers as part of this).
@joachim’s 2023 suggestion was to split the problem into separate issues for each test type. I tried to summarize would this would look like using ChatGPT to highlight what this would look like, along with justification and next steps. I'm mostly doing this to nudge this ahead.
Do these new issues sound like good places to begin? Has there been other work to do this already?
⸻
1. Kernel Tests: Optimize setUp() Execution
Goal: Avoid redundant setUp() execution for each method in Kernel tests.
Justification:
• Kernel tests do not boot the full stack and have fewer side effects.
• A shared setUp() could save significant time across large test classes.
Suggested New Issue:
“Optimize PHPUnit Kernel test execution by avoiding repeated setUp() calls per method.”
Action Items:
• Benchmark time saved when sharing setUp() across methods.
• Propose an attribute or annotation to allow opting in/out per class.
• Ensure tearDown cleanup logic remains reliable.
⸻
2. Browser Tests (Functional + FunctionalJavascript): Improve Test Efficiency
Goal: Investigate safe ways to cache installed sites between test methods in browser-based test classes.
Justification:
• Browser tests are the slowest due to full Drupal installs and HTTP interactions.
• Current design reinstalls Drupal for every method—even within the same class.
Suggested New Issue:
“Enable shared Drupal site installations across Browser test methods to reduce test run time.”
Action Items:
• Explore integration with #2900208 for site install caching.
• Identify classes that could safely share a site environment.
• Flag test classes with dependencies on state or side effects to avoid breaking isolation.
⸻
3. JavaScript Tests: Evaluate Efficiency Gains from Shared setUp()
Goal: Identify whether JS tests (Nightwatch or others) could benefit from setup sharing, or if they already do.
Justification:
• JS test infrastructure is separate (Node/Nightwatch/WebDriver), but setup redundancy may still exist.
• Even if not using PHPUnit, parallels in redundant test bootstrapping may exist.
Suggested New Issue:
“Audit and optimize Nightwatch JS tests for redundant setup executions across test methods.”
Action Items:
• Confirm whether test fixtures or browser states are rebuilt between methods.
• Identify opportunities for fixture reuse or browser session persistence.
⸻
Cross-Issue Meta Task: Central Coordination and Tracking
Create a meta issue to link all three above:
“Optimize test performance by limiting redundant setUp() execution across test types.”
⸻
Justification for Splitting
Joachim’s 2023 comment was correct: different test types have different performance bottlenecks, state requirements, and risk profiles. Attempting to solve them all in one issue creates blockers and lack of clarity. Splitting enables:
• Focused benchmarking
• Different contributors to work in parallel
• Faster consensus and review
• Targeted test infrastructure changes
Comment #60
catchThe chatgpt output isn't helpful.
#2900208: Improve performance of functional tests by caching Drupal installations had more recent work on it, but the performance gains there aren't clear.
#3416522: Add the ability to install multiple modules and only do a single container rebuild to ModuleInstaller significantly reduced setup costs for both functional and functional js tests (possibly kernel tests too although it would be a smaller proportion of the test time).
The bigger problem here is that a lot of tests rely on starting off with a fresh environment, so individual test methods would have to be rewritten to account for changes made in other methods. This means the new attribute would have to be opt-in, which means no performance gain until it's applied individually to each test and those tests are potentially refactored - it's a lot of work and it's possible to optimize individual tests to have less methods individually without these APIs by doing more in one method.
I did some work on trying to optimize some of the very slowest tests over the past couple of years, and you can see how much work it is per test:
#3462556: Consolidate two test methods in NumberFieldTest
#3460654: Merge test methods in FieldUIRouteTest for better performance
#3463351: Consolidate Umami performance tests
#3463456: Consolidate ckeditor5's FunctionalJavascript tests
There are other similar ones around that stalled. So finding the slowest tests and speeding them up is for me a more efficient use of time. The ability to do setup/teardown once for a test could be helpful for some of those, but it's only part of the work - and you can just make test methods protected and call them from one public method to get pretty much the same effect.
#3390193: Add a drupalGet() method to KernelTestBase is likely to have much more impact too.
#3462762: [meta] Core test suite performance isn't up-to-date but has more issue references.
Comment #61
mgiffordThanks @catch I appreciate the better update!
Comment #62
chi commentedThere are a few critical points that I believe deserve attention.
1. Need for fast tests across all kind of modules
Fast tests are essential not only for Drupal core but also for contributed and custom modules. In my projects, I often avoid using data providers in kernel tests with large datasets because they become prohibitively slow. This limitation is frustrating.
2. Outdated testing System
The current testing framework has seen little improvement since the PHPUnit migration. It has become a tangled mess of gigantic base classes, traits, and poorly documented environment variables. A deep refactoring is necessary, and, I think, we should consider building a new testing framework from scratch. This framework should provide:
• Factories for generating demo content.
• Comprehensive assertion helpers.
• A helper to install Drupal.
• Strict separation between tests and the testing framework. See #3067979: Exclude test files from release packages.
3. A streamlined way to install Drupal with specified parameters.
Obsolete install system blocks progress. While not directly related to testing performance, the outdated installation subsystem is a blocker for improving the testing framework. It still relies on global variables, legacy functions like
install_tasks(), and patterns dating back to Drupal 6 (or earlier). The new testing framework needs a clean way to spin up isolated Drupal instances.Comment #63
catch@chi yeah I also stopped using data providers for several tests in core like #3462561: Stop using a data provider in UserPasswordResetTest, agreed it's a pain and if you don't pay attention to test runtimes it can add minutes to runtimes.
I don't really see how e.g. KernelTestBase::setUp() can be implemented once per test class though, because it's setting a load of class properties. But if we were going to try to do this, then kernel tests are probably the right place to start because there is often less custom logic in setUp().
For me though I think I would consider making it a new base class like SingleEnvironmentKernelTestBase or similar so we don't have loads of logic branching.
Installer, yes it's bad, issues like #3136855: Refactor install_* database helpers into an install helper class and child issues of #2876001: Installerless Drupal need help.
Comment #64
catchTo expand on #63. Actual unit tests are generally fine with dataproviders because there's limited things you can do in ::setUp().
Functional and functional js tests there are much less use cases for data providers, and at least for functional tests, they can be converted to kernel tests once #3390193: Add a drupalGet() method to KernelTestBase is available.
So if we have a SingleEnvironmentKernelTestBase, and also do #3390193: Add a drupalGet() method to KernelTestBase, and then convert quite a lot of core and contrib tests to use those two things, that should give a good idea if there are any gaps left.
Comment #65
joachim commented> In my projects, I often avoid using data providers in kernel tests with large datasets because they become prohibitively slow.
Agreed, and for me data providers are also the key use case for this issue's functionality.
> It has become a tangled mess of gigantic base classes, traits, and poorly documented environment variables. A deep refactoring is necessary
Yes!!
But I think rebuilding from scratch would be insanely complex - there would be lots of functionality to discover and ensure we don't lose. I think a steady refactoring can work too -- there is an issue for standardizing which folders we put traits in, for example. Standardizing naming and locations of things would be a big first step.
Comment #66
catchMoving classes and traits around should be straightforward now we have #3502882: Add a classloader that can handle class moves but also that discussion needs its own issue. Probably as a child/related issue of #3067979: Exclude test files from release packages.