Skip to content

Commit a9d7ed0

Browse files
Copilotlinkdotnet
andcommitted
test: Add comprehensive unit tests for BUNIT0002 analyzer
Co-authored-by: linkdotnet <[email protected]>
1 parent 088c562 commit a9d7ed0

File tree

7 files changed

+355
-14
lines changed

7 files changed

+355
-14
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,6 @@
102102
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0"/>
103103
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0"/>
104104
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
105+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />
105106
</ItemGroup>
106107
</Project>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
uid: bunit-analyzers
3+
title: bUnit Analyzers
4+
---
5+
6+
# bUnit Analyzers
7+
8+
The `bunit.analyzers` package contains a set of Roslyn analyzers that help identify common mistakes and anti-patterns when writing bUnit tests. The analyzers are designed to provide early feedback during development, catching issues before tests are run. To use the analyzers, install the `bunit.analyzers` NuGet package in your test project.
9+
10+
This page describes the available analyzers and their usage.
11+
12+
## Installation
13+
14+
Install the package via NuGet:
15+
16+
```bash
17+
dotnet add package bunit.analyzers
18+
```
19+
20+
The analyzers will automatically run during compilation and provide warnings or suggestions in your IDE and build output.
21+
22+
## Available Analyzers
23+
24+
### BUNIT0002: Prefer Find&lt;T&gt; over casting
25+
26+
**Severity**: Info
27+
**Category**: Usage
28+
29+
This analyzer detects when you cast the result of `Find(selector)` and suggests using the generic `Find<T>(selector)` method instead. Using the generic method is more concise, type-safe, and expresses intent more clearly.
30+
31+
#### Examples
32+
33+
**❌ Incorrect** - triggers BUNIT0002:
34+
```csharp
35+
using AngleSharp.Dom;
36+
37+
var cut = RenderComponent<MyComponent>();
38+
IHtmlAnchorElement link = (IHtmlAnchorElement)cut.Find("a");
39+
```
40+
41+
**✅ Correct**:
42+
```csharp
43+
using AngleSharp.Dom;
44+
45+
var cut = RenderComponent<MyComponent>();
46+
var link = cut.Find<IHtmlAnchorElement>("a");
47+
```
48+
49+
#### When to Use
50+
51+
Use `Find<T>()` whenever you need a specific element type:
52+
- When working with AngleSharp element interfaces (`IHtmlAnchorElement`, `IHtmlButtonElement`, etc.)
53+
- When you need to access type-specific properties or methods
54+
- When you want clearer, more maintainable test code
55+
56+
### BUNIT0001: Razor test files should inherit from BunitContext
57+
58+
**Status**: 🚧 Planned for future release
59+
60+
This analyzer will detect when Razor test files (`.razor` files) use variables or event callbacks from the test code without inheriting from `BunitContext`. Without the proper inheritance, you may encounter the error "The render handle is not yet assigned."
61+
62+
#### Planned Examples
63+
64+
**❌ Incorrect** - will trigger BUNIT0001 in the future:
65+
```razor
66+
@code
67+
{
68+
[Fact]
69+
public void Test()
70+
{
71+
using var ctx = new BunitContext();
72+
73+
Action<MouseEventArgs> onClickHandler = _ => { Assert.True(true); };
74+
75+
var cut = ctx.Render(@<MyComponent OnClick="onClickHandler" />);
76+
cut.Find("button").Click();
77+
}
78+
}
79+
```
80+
81+
**✅ Correct**:
82+
```razor
83+
@inherits BunitContext
84+
@code
85+
{
86+
[Fact]
87+
public async Task Test()
88+
{
89+
var wasInvoked = false;
90+
91+
Action<MouseEventArgs> onClick = _ => { wasInvoked = true; };
92+
93+
var cut = Render(@<MyComponent OnClick="onClick" />);
94+
var button = cut.Find("button");
95+
await button.ClickAsync(new MouseEventArgs());
96+
97+
cut.WaitForAssertion(() => Assert.True(wasInvoked));
98+
}
99+
}
100+
```
101+
102+
## Configuration
103+
104+
The analyzers can be configured in your project's `.editorconfig` file or using ruleset files. For example, to change the severity of BUNIT0002:
105+
106+
```ini
107+
# .editorconfig
108+
[*.cs]
109+
# Change BUNIT0002 from Info to Warning
110+
dotnet_diagnostic.BUNIT0002.severity = warning
111+
112+
# Or disable it entirely
113+
dotnet_diagnostic.BUNIT0002.severity = none
114+
```
115+
116+
## Contributing
117+
118+
We welcome contributions! If you have ideas for additional analyzers that could help bUnit users, please:
119+
120+
1. Check existing [issues](https://github.com/bunit-dev/bUnit/issues) for similar suggestions
121+
2. Open a new issue describing the problem the analyzer would solve
122+
3. Submit a pull request with your implementation
123+
124+
Common areas for improvement include:
125+
- Detecting incorrect usage of `WaitFor` methods
126+
- Identifying missing or incorrect component parameter bindings
127+
- Catching improper service injection patterns
128+
- Finding opportunities to use helper methods
129+
130+
## Feedback
131+
132+
If you encounter issues with the analyzers or have suggestions for improvements, please [open an issue](https://github.com/bunit-dev/bUnit/issues/new) on GitHub.

docs/site/docs/extensions/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ title: Extensions for bUnit
77

88
This section covers the various extensions available for bUnit. These extensions are not part of the core bUnit package, but are instead available as separate NuGet packages. The extensions are listed below, and each has its own documentation page.
99

10+
* **[bunit.analyzers](xref:bunit-analyzers)** - A set of Roslyn analyzers that help identify common mistakes and anti-patterns when writing bUnit tests
1011
* **[bunit.generators](xref:bunit-generators)** - A set of source generators that can be used to generate code like stubs for Blazor components

src/bunit.analyzers/DiagnosticDescriptors.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Bunit.Analyzers;
55
/// <summary>
66
/// Diagnostic descriptors for bUnit analyzers.
77
/// </summary>
8-
internal static class DiagnosticDescriptors
8+
public static class DiagnosticDescriptors
99
{
1010
private const string Category = "Usage";
1111

tests/bunit.analyzers.tests/AnalyzerTests.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Testing;
4+
using Xunit;
5+
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier<Bunit.Analyzers.PreferGenericFindAnalyzer>;
6+
7+
namespace Bunit.Analyzers.Tests;
8+
9+
public class PreferGenericFindAnalyzerTests
10+
{
11+
[Fact]
12+
public async Task NoDiagnostic_WhenUsingGenericFind()
13+
{
14+
const string code = @"
15+
namespace TestNamespace
16+
{
17+
public class TestClass
18+
{
19+
public void TestMethod()
20+
{
21+
var cut = new TestHelper();
22+
var elem = cut.Find<string>(""a"");
23+
}
24+
}
25+
26+
public class TestHelper
27+
{
28+
public T Find<T>(string selector) => default(T);
29+
}
30+
}";
31+
32+
await VerifyCS.VerifyAnalyzerAsync(code);
33+
}
34+
35+
[Fact]
36+
public async Task NoDiagnostic_WhenCastingNonFindMethod()
37+
{
38+
const string code = @"
39+
namespace TestNamespace
40+
{
41+
public class TestClass
42+
{
43+
public void TestMethod()
44+
{
45+
var obj = new TestHelper();
46+
var elem = (string)obj.GetSomething();
47+
}
48+
}
49+
50+
public class TestHelper
51+
{
52+
public object GetSomething() => null;
53+
}
54+
}";
55+
56+
await VerifyCS.VerifyAnalyzerAsync(code);
57+
}
58+
59+
[Fact]
60+
public async Task NoDiagnostic_WhenFindIsNotFromRenderedFragment()
61+
{
62+
const string code = @"
63+
namespace TestNamespace
64+
{
65+
public class TestClass
66+
{
67+
public void TestMethod()
68+
{
69+
var helper = new UnrelatedHelper();
70+
var result = (string)helper.Find(""test"");
71+
}
72+
}
73+
74+
public class UnrelatedHelper
75+
{
76+
public object Find(string selector) => null;
77+
}
78+
}";
79+
80+
await VerifyCS.VerifyAnalyzerAsync(code);
81+
}
82+
83+
[Fact]
84+
public async Task Diagnostic_WhenCastingFindResultFromIRenderedFragment()
85+
{
86+
const string code = @"
87+
namespace TestNamespace
88+
{
89+
public interface IMyElement { }
90+
91+
public class TestClass
92+
{
93+
public void TestMethod()
94+
{
95+
var cut = new MockRenderedFragment();
96+
IMyElement elem = {|#0:(IMyElement)cut.Find(""a"")|};
97+
}
98+
}
99+
100+
public interface IRenderedFragment
101+
{
102+
object Find(string selector);
103+
}
104+
105+
public class MockRenderedFragment : IRenderedFragment
106+
{
107+
public object Find(string selector) => null;
108+
}
109+
}";
110+
111+
var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.PreferGenericFind.Id)
112+
.WithLocation(0)
113+
.WithArguments("IMyElement", "\"a\"");
114+
115+
await VerifyCS.VerifyAnalyzerAsync(code, expected);
116+
}
117+
118+
[Fact]
119+
public async Task Diagnostic_WhenCastingFindResultFromRenderedComponent()
120+
{
121+
const string code = @"
122+
namespace TestNamespace
123+
{
124+
public interface IMyElement { }
125+
126+
public class TestClass
127+
{
128+
public void TestMethod()
129+
{
130+
var cut = new MockRenderedComponent();
131+
var elem = {|#0:(IMyElement)cut.Find(""div"")|};
132+
}
133+
}
134+
135+
public interface IRenderedComponent
136+
{
137+
object Find(string selector);
138+
}
139+
140+
public class MockRenderedComponent : IRenderedComponent
141+
{
142+
public object Find(string selector) => null;
143+
}
144+
}";
145+
146+
var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.PreferGenericFind.Id)
147+
.WithLocation(0)
148+
.WithArguments("IMyElement", "\"div\"");
149+
150+
await VerifyCS.VerifyAnalyzerAsync(code, expected);
151+
}
152+
153+
[Fact]
154+
public async Task Diagnostic_WhenCastingFindResultFromRenderedFragmentType()
155+
{
156+
const string code = @"
157+
namespace TestNamespace
158+
{
159+
public interface IMyElement { }
160+
161+
public class TestClass
162+
{
163+
public void TestMethod()
164+
{
165+
var cut = new RenderedFragment();
166+
var button = {|#0:(IMyElement)cut.Find(""button"")|};
167+
}
168+
}
169+
170+
public class RenderedFragment
171+
{
172+
public object Find(string selector) => null;
173+
}
174+
}";
175+
176+
var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.PreferGenericFind.Id)
177+
.WithLocation(0)
178+
.WithArguments("IMyElement", "\"button\"");
179+
180+
await VerifyCS.VerifyAnalyzerAsync(code, expected);
181+
}
182+
183+
[Fact]
184+
public async Task Diagnostic_WithComplexSelector()
185+
{
186+
const string code = @"
187+
namespace TestNamespace
188+
{
189+
public interface IMyElement { }
190+
191+
public class TestClass
192+
{
193+
public void TestMethod()
194+
{
195+
var cut = new MockRenderedFragment();
196+
var link = {|#0:(IMyElement)cut.Find(""a.nav-link[href='/home']"")|};
197+
}
198+
}
199+
200+
public interface IRenderedFragment
201+
{
202+
object Find(string selector);
203+
}
204+
205+
public class MockRenderedFragment : IRenderedFragment
206+
{
207+
public object Find(string selector) => null;
208+
}
209+
}";
210+
211+
var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.PreferGenericFind.Id)
212+
.WithLocation(0)
213+
.WithArguments("IMyElement", "\"a.nav-link[href='/home']\"");
214+
215+
await VerifyCS.VerifyAnalyzerAsync(code, expected);
216+
}
217+
}

0 commit comments

Comments
 (0)