[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
🧪 Test quality grade — PR #9382
This advisory comment was generated automatically. Grades are heuristic
|
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.
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