Uh oh!
There was an error while loading. Please reload this page.
[test-improver] test: add edge case tests for PreferTestCleanupOverDispose and PreferTestInitializeOverConstructor analyzers - #9382
Conversation
…TestInitializeOverConstructor analyzers Add 3 new edge case tests across two related 'prefer test lifecycle over C# lifecycle' analyzers: - PreferTestCleanupOverDisposeAnalyzerTests: - WhenTestClassHasFullDisposePattern_OnlyParameterlessDisposeDiagnostic: verifies that in the classic full dispose pattern (Dispose() + Dispose(bool)), only the parameterless IDisposable.Dispose() implementation is flagged; the protected Dispose(bool) helper is not reported and is preserved intact by the fixer. - WhenTestClassHasBothDisposeAndTestCleanup_DiagnosticOnDispose: verifies that Dispose() is still flagged even when a [TestCleanup] method already exists — users should not implement both cleanup patterns simultaneously. - PreferTestInitializeOverConstructorAnalyzerTests: - WhenTestClassHasStaticConstructor_NoDiagnostic: verifies that a static constructor in a test class does not trigger the rule, since the analyzer only targets explicit parameterless *instance* constructors (Roslyn's InstanceConstructors property excludes static constructors by design). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds additional unit-test coverage for MSTest.Analyzers rules that encourage MSTest lifecycle methods over C# lifecycle constructs, specifically targeting previously untested edge cases in the constructor and dispose analyzers.
Changes:
- Add a no-diagnostic test ensuring a
[TestClass]static constructor is ignored byPreferTestInitializeOverConstructor. - Add a code-fix test covering the full
IDisposablepattern to ensure onlyDispose()is flagged/fixed (notDispose(bool)). - Add an analyzer-only test ensuring
Dispose()is still flagged even when a[TestCleanup]method already exists.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/MSTest.Analyzers.UnitTests/PreferTestInitializeOverConstructorAnalyzerTests.cs | Adds coverage that static constructors do not trigger the “prefer TestInitialize over ctor” rule. |
| test/UnitTests/MSTest.Analyzers.UnitTests/PreferTestCleanupOverDisposeAnalyzerTests.cs | Adds edge-case coverage for full dispose-pattern handling and for coexistence of [TestCleanup] + Dispose(). |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 0
Amaury Levé (Evangelink)
commented
Jun 24, 2026
🧪 Test quality grade — PR #9382
This advisory comment was generated automatically. Grades are heuristic
|
Amaury Levé (Evangelink)
left a comment
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
Verdict table
| # | Dimension | Status | Notes |
|---|---|---|---|
| 1 | Algorithmic Correctness | ✅ | Tests target the correct code paths; fixed-code expectations match fixer output |
| 2 | Threading & Concurrency | N/A | Test-only change |
| 3 | Security & IPC Contract Safety | N/A | — |
| 4 | Public API & Binary Compatibility | N/A | No public API touched |
| 5 | Performance & Allocations | N/A | — |
| 6 | Cross-TFM Compatibility | ✅ | New tests use IDisposable (not IAsyncDisposable), correctly placed outside the #if NET guard |
| 7 | Resource & IDisposable Management | N/A | — |
| 8 | Defensive Coding at Boundaries | N/A | — |
| 9 | Error Handling & Diagnostics | N/A | — |
| 10 | Test Coverage & Quality | See notes below | |
| 11 | Naming & Code Style | ✅ | Method names follow the WhenXxx_YyyVerb convention; consistent with the existing file |
| 12 | Documentation & Comments | ✅ | Inline comments are appropriate for non-obvious edge cases |
| 13 | Localization | N/A | — |
| 14 | Logging & Observability | N/A | — |
| 15 | Configuration & Feature Flags | N/A | — |
| 16 | Migration & Upgrade Safety | N/A | — |
| 17 | Dependency & Package Hygiene | N/A | — |
| 18 | CI/CD & Build Integration | N/A | — |
| 19 | Backward Compatibility | N/A | — |
| 20 | Type System & Nullability | N/A | — |
| 21 | Accessibility & Extensibility | N/A | — |
| 22 | PowerShell & Script Hygiene | N/A | — |
Detailed findings
Test 1 — WhenTestClassHasFullDisposePattern_OnlyParameterlessDisposeDiagnostic
The fixedCode expectation faithfully represents what the fixer produces:
IDisposableremoved from the base-type list.- Parameterless
Dispose()renamed to[TestCleanup] TestCleanup()with its body preserved. protected virtual Dispose(bool)left untouched (the fixer only targets the flagged method).using System;correctly retained —GC.SuppressFinalize(this)in the fixed body still referencesSystem.GC.
One semantic oddity worth being aware of: after the fix, TestCleanup() calls GC.SuppressFinalize(this) even though the class no longer implements IDisposable and has no finalizer, making the call a no-op. This accurately documents the current fixer behaviour rather than a test defect, but it is a signal that the fixer could be smarter about stripping the suppress-finalizer idiom when removing IDisposable.
Test 2 — WhenTestClassHasBothDisposeAndTestCleanup_DiagnosticOnDispose
Using VerifyAnalyzerAsync correctly confirms the diagnostic fires. However, the fixer's behaviour in this scenario is untested. Applying the fixer here would:
- Rename
Dispose()→TestCleanup()(with[TestCleanup]). - Leave the existing
[TestCleanup] Cleanup()untouched. - Produce a class with two
[TestCleanup]-decorated methods (CleanupandTestCleanup) — invalid in MSTest.
The inline comment explains the problem domain but does not explain why VerifyCodeFixAsync is omitted. See the inline comment for a suggested clarification.
Minor inaccuracy in the PR description: the description says "applying the fixer would produce a second
TestCleanup()method". In reality it would produce two differently-named* methods that both carry[TestCleanup](Cleanup+TestCleanup), not two identically named ones. No code issue — just a doc nuance.
Test 3 — WhenTestClassHasStaticConstructor_NoDiagnostic
Clean and correct. VerifyCodeFixAsync(code, code) is the established pattern for no-diagnostic cases in this file and matches WhenTestClassHasImplicitCtor_NoDiagnostic and WhenTestClassHasParameterizedCtor_NoDiagnostic. The test is justified by the fact that INamedTypeSymbol.InstanceConstructors (the Roslyn API the analyzer uses) explicitly excludes static constructors.
Overall verdict
No blocking issues. The three tests are well-targeted, use the correct verifier APIs (VerifyAnalyzerAsync / VerifyCodeFixAsync), follow the project's assertion and naming conventions (MSTest assertions; AwesomeAssertions correctly absent), and meaningfully improve edge-case coverage for two related analyzers. The one suggestion (documenting or testing the fixer gap in test 2) is filed as an inline comment.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Goal and Rationale
Add 3 edge case tests across two related "prefer test lifecycle over C# lifecycle" analyzer test files (
PreferTestCleanupOverDisposeAnalyzerTestsandPreferTestInitializeOverConstructorAnalyzerTests). These tests cover real code paths through the analyzers that were previously untested.Approach
PreferTestCleanupOverDisposeAnalyzerTests — 2 new tests:
WhenTestClassHasFullDisposePattern_OnlyParameterlessDisposeDiagnosticThe full dispose pattern uses two methods:
public void Dispose()(theIDisposableinterface implementation) andprotected virtual void Dispose(bool disposing)(the parameterized cleanup helper). The analyzer checksIsDisposeImplementation, which only matches the interface implementation. This test verifies:Dispose()is flagged;Dispose(bool)is notDispose()toTestCleanup()and leavesDispose(bool)intactWhenTestClassHasBothDisposeAndTestCleanup_DiagnosticOnDisposeWhen a test class has both a
[TestCleanup]method and implementsIDisposable.Dispose(), the analyzer should still flagDispose()— the existence ofTestCleanupdoes not suppress the diagnostic. This guards against the misconception that having aTestCleanupmakes theDisposepattern acceptable.PreferTestInitializeOverConstructorAnalyzerTests — 1 new test:
WhenTestClassHasStaticConstructor_NoDiagnosticThe analyzer targets only explicit parameterless instance constructors via
InstanceConstructors.FirstOrDefault(x => !x.IsImplicitlyDeclared && x.Parameters.Length == 0). Roslyn'sInstanceConstructorsproperty excludes static constructors by design. This test verifies that a static constructor in a[TestClass]does not trigger the rule.Test Results
All 14 tests pass (7 per file, 3 new + 11 existing):
Command used:
Trade-offs
WhenTestClassHasBothDisposeAndTestCleanup_DiagnosticOnDisposetest usesVerifyAnalyzerAsync(notVerifyCodeFixAsync) since applying the fixer would produce a secondTestCleanup()method — a known limitation worth documenting through the test itself.Add this agentic workflows to your repo
To install this agentic workflow, run