Summary
DrupalTrait::tearDownDrupal() deletes every marked fixture entity one at a time:
foreach ($this->cleanupEntities as $entity) { $entity->delete(); }
For N fixtures, that's N separate single-entity storage deletes. EntityStorageInterface::delete() already accepts an array and fires the same per-entity hooks whether passed one entity or many, so the loop is pure round-trip overhead — worst in entity-heavy tests that create many nodes/users per method.
Proposed fix
Iterate in reverse creation order and issue one $storage->delete($batch) call per run of consecutive same-type entities:
$entity_type_manager = $this->container->get('entity_type.manager'); $batch = []; $batch_entity_type_id = null; foreach (array_reverse($this->cleanupEntities) as $entity) { $entity_type_id = $entity->getEntityTypeId(); if ($entity_type_id !== $batch_entity_type_id && $batch !== []) { $entity_type_manager->getStorage($batch_entity_type_id)->delete($batch); $batch = []; } $batch_entity_type_id = $entity_type_id; $batch[] = $entity; } if ($batch !== []) { $entity_type_manager->getStorage($batch_entity_type_id)->delete($batch); }
Risk
Ordering is preserved — later-created entities still delete first, and each call handles only one type, so cross-type dependency ordering can't be violated. $storage->delete([$a, $b]) fires the same per-entity hooks as N individual calls, and bucketing only consecutive same-type runs keeps interleaved-type ordering intact.
Measured impact
Near-zero saving on tests with one or two entities, largest on entity-heavy classes. Measured together with two related fixes (pre-flight visit skip, watchdog memoization), the combined per-test body time dropped roughly 5.75s → ~3.9s on a smoke-test probe. Verified with a behavior test driving the real tearDownDrupal() with interleaved node/user fixtures, confirming every one is gone afterward.
Patch
Attached: dtt-batched-entity-cleanup.patch (against 2.7.0 tip). Replaces the per-entity delete loop in src/DrupalTrait.php::tearDownDrupal() with the batched approach above. Includes the interleaved-fixture behavior test.
AI-assistance disclosure
Per drupal.org's policy on AI use in contributions: AI-Generated: Yes. An AI coding assistant helped draft this patch and write-up under my direction. I reviewed the change, ran its behavior test locally, and confirmed the measured numbers come from real test runs — not generated. I take full responsibility for its correctness and will respond to maintainer feedback on this issue.
| Comment | File | Size | Author |
|---|---|---|---|
| dtt-batched-entity-cleanup.patch | 1.82 KB | grasmash |
Issue fork dtt-3608416
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 #3
grasmash commentedComment #6
moshe weitzman commentedComment #8
acbramley commentedThis has caused some issues with deleting menu link content entities, I'll try to track down why but the error looks like this:
Drupal\Core\Entity\EntityStorageException: Entity not found through the menu link plugin definition and could not fallback on UUID '09ce18b5-ebb7-48fc-9f84-afc5ae474048'Comment #9
acbramley commentedI think this needs to be reverted and re-thought. The issue seems to be that now the MenuLinkContent entities that are related to nodes are deleted first, but because of static caching, menu_link_content_entity_predelete still loads the link plugins in loadLinksByRoute when the node gets deleted and then that exception is thrown in MenuLinkContent::getEntity.
Comment #10
mstrelan commentedThis also broke one of my tests:
+1 to revert. I'm not really sure it was worthwhile in the first place? Think we need to see more detailed profiling.
Comment #12
mstrelan commentedComment #14
acbramley commentedRevert is in, back to Needs work to rethink the solution.