Skip to content

[PHPUnit60] Skip tests in classes with asserting lifecycle methods in AddDoesNotPerformAssertionToNonAssertingTestRector - #827

Open
bbrala wants to merge 1 commit into
rectorphp:mainfrom
bbrala:does-not-perform-assertions-lifecycle
Open

bbrala wants to merge 1 commit into
rectorphp:mainfrom
bbrala:does-not-perform-assertions-lifecycle

Conversation

@bbrala

@bbrala bbrala commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Part of rectorphp/rector#9965, split from #822. Stacked on #826: only the last commit (e6105ff) is new here.

PHPUnit counts assertions made in lifecycle methods towards the test. A test whose own body asserts nothing still "performs assertions" when setUp(), tearDown(), assertPreConditions(), assertPostConditions(), a #[Before]/#[After]/#[PreCondition]/#[PostCondition] method or a @before/@after method asserts, in the class, an ancestor or a used trait. AddDoesNotPerformAssertionToNonAssertingTestRector adds #[DoesNotPerformAssertions] to those tests, and with failOnRisky they then fail.

Change

New LifecycleMethodAssertAnalyzer. A test is skipped when its class, an ancestor or a used trait has an asserting lifecycle method.

  • The test's own class is taken from the current file's AST (found in getFile()->getNewStmts()), so the file being processed is not parsed again. The rule stays on ClassMethod.
  • Ancestors and traits go through AstResolver, but only when a cheap regex on the file contents finds a hook name or a Before/After/PreCondition/PostCondition attribute or annotation. The regex can give false positives (an extra parse), never miss a hook.
  • An overriding hook hides the ancestor's hook unless it calls parent::<hook>(). Each hook is evaluated on its own, so long parent::setUp() chains are not cut off by AssertCallAnalyzer's nesting limit.
  • Results are cached per class plus the set of hooks overridden below it.
  • In a trait parsed on its own, $this is unknown. $this->x(), self::x() and static::x() are resolved against the trait's own methods; if that fails (a method of the using class), the call is treated as possibly asserting.

Two behaviour changes worth a look:

  • Test methods in abstract classes are now skipped: a child class can add an asserting hook, and the attribute on the abstract method would then fail the child's tests.
  • A trait helper calling a method of the using class (or of TestCase) now counts as possibly asserting, so such tests keep missing the attribute. This only ever removes a change, never adds a wrong one.

Bench

The #824 bench now has 3 scenarios: the original inherited-test shape, every test class with its own setUp(), and asserting hooks at the top of each parent chain. It builds a File per test class and sets it on CurrentFileProvider, like AbstractRectorTestCase does, because the rule now reads the current file. CPU, 3 runs, local ddev PHP 8.5:

scenario main #826 this PR
inherited 0.12 s 0.24 s 0.48 s
own setUp() 0.12 s 0.24 s 0.51 s
asserting hooks 0.13 s 0.21 s 0.49 s

On main and #826 the "asserting hooks" scenario changes all 1800 methods; with this PR none. The remaining cost is parsing the ~30 parent files that declare hooks.

Tests

New fixtures cover: own and inherited setUp()/tearDown()/assertPostConditions(), #[Before] (also aliased), @after, a 7-level parent::setUp() chain, hooks and helpers in traits (same file, other file, used by a parent), parent::setUp() in a trait hook, overrides without a parent call, anonymous classes, and abstract classes. Full suite (727 tests), ECS, Rector, class-leak and finalize-classes pass. PHPStan: the same 6 pre-existing errors as main.

Context

Found running the composer-based PHPUnit set on Drupal core (https://www.drupal.org/i/3638148), which runs with failOnRisky and has many base classes asserting in setUp() or assertPostConditions().

@TomasVotruba
TomasVotruba force-pushed the does-not-perform-assertions-lifecycle branch from e6105ff to d17bdaf Compare November 14, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant