Skip to content

Commit 4e000f6

Browse files
authored
Smarter render/change tracking and auto-refreshing queries (#59)
* Initial changes * Switched to IObservable<RenderEvent> instead of Task NextRender * Added comments to code * cleanup * Added RenderEvents to IRenderedFragment and moved change and render detection logic to RenderEvent * VerifyAsyncChanges added * Moved files around * Added WaitForState helper * Made checks safe across cpu boundaries. * Changed VerifyAsyncChange to WaitForAssertion, upgraded dependencies to latest version * Template update * Added wrapping, removed http mocking * Added better error messages through custom exceptions * Moved class to own file * Updates to sample * Changed waiting operations to retest on render, not no change * Added obsolete to WaitForNextRender, exposed WaitForState and WaitForAssertion at ITextContext level * Added changelog to project * Added Template helper method * Added general support for XunitLogger to razor and snapshot tests * First set of blazor e2e tests, smarter FindAll * Using cleanup * Tweaks to test * Changed anglesharp.wrappers version dep * Tweaks to template * Update CI.yml * Update nuget-pack-push.yml
1 parent 053cf3c commit 4e000f6

File tree

168 files changed

+3405
-802
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

168 files changed

+3405
-802
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ jobs:
3838
- name: Verifying template
3939
run: |
4040
dotnet new --install ${GITHUB_WORKSPACE}/template/bunit.template.$VERSION.nupkg
41-
dotnet new razortest -o ${GITHUB_WORKSPACE}/Test
41+
dotnet new bunit -o ${GITHUB_WORKSPACE}/Test
4242
dotnet restore ${GITHUB_WORKSPACE}/Test/Test.csproj --source ${GITHUB_WORKSPACE}/lib
4343
dotnet test ${GITHUB_WORKSPACE}/Test

.github/workflows/nuget-pack-push.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
- name: Verifying template
4242
run: |
4343
dotnet new --install ${GITHUB_WORKSPACE}/template/bunit.template.$VERSION.nupkg
44-
dotnet new razortest -o ${GITHUB_WORKSPACE}/Test
44+
dotnet new bunit -o ${GITHUB_WORKSPACE}/Test
4545
dotnet restore ${GITHUB_WORKSPACE}/Test/Test.csproj --source ${GITHUB_WORKSPACE}/lib
4646
dotnet test ${GITHUB_WORKSPACE}/Test
4747
- name: Push packages to NuGet.org

CHANGELOG.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Changelog
2+
All notable changes to **bUnit** will be documented in this file. The project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3+
4+
## [Unreleased]
5+
This release includes a **name change from Blazor Components Testing Library to bUnit**. It also brings along two extra helper methods for working with asynchronously rendering components during testing, and a bunch of internal optimizations and tweaks to the code.
6+
7+
*Why change the name?* Naming is hard, and I initial chose a very product-namy name, that quite clearly stated what the library was for. However, the name isn't very searchable, since it just contains generic keywords, plus, bUnit is just much cooler. It also gave me the opportunity to remove my name from all the namespaces and simplify those.
8+
9+
### NuGet
10+
The latest version of the library is availble on NuGet:
11+
12+
| | Type | Link |
13+
| ------------- | ----- | ---- |
14+
| [![Nuget](https://img.shields.io/nuget/dt/bunit?logo=nuget&style=flat-square)](https://www.nuget.org/packages/bunit/) | Library | [https://www.nuget.org/packages/bunit/](https://www.nuget.org/packages/bunit/) |
15+
| [![Nuget](https://img.shields.io/nuget/dt/bunit.template?logo=nuget&style=flat-square)](https://www.nuget.org/packages/bunit.template/) | Template | [https://www.nuget.org/packages/bunit.template/](https://www.nuget.org/packages/bunit.template/) |
16+
17+
### Added
18+
- **`WaitForState(Func<bool> statePredicate, TimeSpan? timeout = 1 second)` has been added to `ITestContext` and `IRenderedFragment`.**
19+
This method will wait (block) until the provided statePredicate returns true, or the timeout is reached (during debugging the timeout is disabled). Each time the renderer in the test context renders, or the rendered fragment renders, the statePredicate is evaluated.
20+
21+
You use this method, if you have a component under test, that requires _one or more asynchronous triggered renders_, to get to a desired state, before the test can continue.
22+
23+
The following example tests the `DelayedRenderOnClick.razor` component:
24+
25+
```cshtml
26+
// DelayedRenderOnClick.razor
27+
<p>Times Clicked: @TimesClicked</p>
28+
<button @onclick="ClickCounter">Trigger Render</button>
29+
@code
30+
{
31+
public int TimesClicked { get; private set; }
32+
33+
async Task ClickCounter()
34+
{
35+
await Task.Delay(1); // wait 1 millisecond
36+
TimesClicked += 1;
37+
}
38+
}
39+
```
40+
41+
This is a test that uses `WaitForState` to wait until the component under test has a desired state, before the test continues:
42+
43+
```csharp
44+
[Fact]
45+
public void WaitForStateExample()
46+
{
47+
// Arrange
48+
var cut = RenderComponent<DelayedRenderOnClick>();
49+
50+
// Act
51+
cut.Find("button").Click();
52+
cut.WaitForState(() => cut.Instance.TimesClicked == 1);
53+
54+
// Assert
55+
cut.Find("p").TextContent.ShouldBe("Times Clicked: 1");
56+
}
57+
```
58+
59+
- **`WaitForAssertion(Action assertion, TimeSpan? timeout = 1 second)` has been added to `ITestContext` and `IRenderedFragment`.**
60+
This method will wait (block) until the provided assertion method passes, i.e. runs without throwing an assert exception, or until the timeout is reached (during debugging the timeout is disabled). Each time the renderer in the test context renders, or the rendered fragment renders, the assertion is attempted.
61+
62+
You use this method, if you have a component under test, that requires _one or more asynchronous triggered renders_, to get to a desired state, before the test can continue.
63+
64+
This is a test that tests the `DelayedRenderOnClick.razor` listed above, and that uses `WaitForAssertion` to attempt the assertion each time the component under test renders:
65+
66+
```csharp
67+
[Fact]
68+
public void WaitForAssertionExample()
69+
{
70+
// Arrange
71+
var cut = RenderComponent<DelayedRenderOnClick>();
72+
73+
// Act
74+
cut.Find("button").Click();
75+
76+
// Assert
77+
cut.WaitForAssertion(
78+
() => cut.Find("p").TextContent.ShouldBe("Times Clicked: 1")
79+
);
80+
}
81+
```
82+
83+
- **Added support for capturing log statements from the renderer and components under test into the test output.**
84+
To enable this, add a constructor to your test classes that takes the `ITestOutputHelper` as input, then in the constructor call `Services.AddXunitLogger` and pass the `ITestOutputHelper` to it, e.g.:
85+
86+
```csharp
87+
// ComponentTest.cs
88+
public class ComponentTest : ComponentTestFixture
89+
{
90+
public ComponentTest(ITestOutputHelper output)
91+
{
92+
Services.AddXunitLogger(output, minimumLogLevel: LogLevel.Debug);
93+
}
94+
95+
[Fact]
96+
public void Test1() ...
97+
}
98+
```
99+
100+
For Razor and Snapshot tests, the logger can be added almost the same way. The big difference is that it must be added during *Setup*, e.g.:
101+
102+
```cshtml
103+
// RazorComponentTest.razor
104+
<Fixture Setup="Setup" ...>
105+
...
106+
</Fixture>
107+
@code {
108+
private ITestOutputHelper _output;
109+
110+
public RazorComponentTest(ITestOutputHelper output)
111+
{
112+
_output = output;
113+
}
114+
115+
void Setup()
116+
{
117+
Services.AddXunitLogger(_output, minimumLogLevel: LogLevel.Debug);
118+
}
119+
}
120+
```
121+
122+
- **Added simpler `Template` helper method**
123+
To make it easier to test components with `RenderFragment<T>` parameters (template components) in C# based tests, a new `Template<TValue>(string name, Func<TValue, string> markupFactory)` helper methods have been added. It allows you to create a mock template that uses the `markupFactory` to create the rendered markup from the template.
124+
125+
This is an example of testing the `SimpleWithTemplate.razor`, which looks like this:
126+
127+
```cshtml
128+
@typeparam T
129+
@foreach (var d in Data)
130+
{
131+
@Template(d);
132+
}
133+
@code
134+
{
135+
[Parameter] public RenderFragment<T> Template { get; set; }
136+
[Parameter] public IReadOnlyList<T> Data { get; set; } = Array.Empty<T>();
137+
}
138+
```
139+
140+
And the test code:
141+
142+
```csharp
143+
var cut = RenderComponent<SimpleWithTemplate<int>>(
144+
("Data", new int[] { 1, 2 }),
145+
Template<int>("Template", num => $"<p>{num}</p>")
146+
);
147+
148+
cut.MarkupMatches("<p>1</p><p>2</p>");
149+
```
150+
151+
Using the more general `Template` helper methods, you need to write the `RenderTreeBuilder` logic yourself, e.g.:
152+
153+
```csharp
154+
var cut = RenderComponent<SimpleWithTemplate<int>>(
155+
("Data", new int[] { 1, 2 }),
156+
Template<int>("Template", num => builder => builder.AddMarkupContent(0, $"<p>{num}</p>"))
157+
);
158+
```
159+
160+
- **Added logging to TestRenderer.** To make it easier to understand the rendering life-cycle during a test, the `TestRenderer` will now log when ever it dispatches an event or renders a component (the log statements can be access by capturing debug logs in the test results, as mentioned above).
161+
162+
- **Added some of the Blazor frameworks end-2-end tests.** To get better test coverage of the many rendering scenarios supported by Blazor, the [ComponentRenderingTest.cs](https://github.com/dotnet/aspnetcore/blob/master/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs) tests from the Blazor frameworks test suite has been converted from a Selenium to a bUnit. The testing style is very similar, so few changes was necessary to port the tests. The two test classes are here, if you want to compare:
163+
164+
- [bUnit's ComponentRenderingTest.cs](/master/tests/BlazorE2E/ComponentRenderingTest.cs)
165+
- [Blazor's ComponentRenderingTest.cs](https://github.com/dotnet/aspnetcore/blob/master/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs)
166+
167+
### Changed
168+
- **Namespaces is now `Bunit`**
169+
The namespaces have changed from `Egil.RazorComponents.Testing.Library.*` to simply `Bunit` for the library, and `Bunit.Mocking.JSInterop` for the JSInterop mocking support.
170+
171+
- **Auto-refreshing `IElement`s returned from `Find()`**
172+
`IRenderedFragment.Find(string cssSelector)` now returns a `IElement`, which internally will refresh itself, whenever the rendered fragment it was found in, changes. This means you can now search for an element once in your test and assign it to a variable, and then continue to assert against the same instance, even after triggering renders of the component under test.
173+
174+
For example, instead of having `cut.Find("p")` in multiple places in the same test, you can do `var p = cut.Find("p")` once, and the use the variable `p` all the places you would otherwise have the `Find(...)` statement.
175+
176+
- **Refreshable element collection returned from `FindAll`.**
177+
The `FindAll` query method on `IRenderedFragment` now returns a new type, the `IRefreshableElementCollection<IElement>` type, and the method also takes a second optional argument now, `bool enableAutoRefresh = false`.
178+
179+
The `IRefreshableElementCollection` is a special collection type that can rerun the query to refresh its the collection of elements that are found by the CSS selector. This can either be done manually by calling the `Refresh()` method, or automatically whenever the rendered fragment renders and has changes, by setting the property `EnableAutoRefresh` to `true` (default set to `false`).
180+
181+
Here are two example tests, that both test the following `ClickAddsLi.razor` component:
182+
183+
```cshtml
184+
<ul>
185+
@foreach (var x in Enumerable.Range(0, Counter))
186+
{
187+
<li>@x</li>
188+
}
189+
</ul>
190+
<button @onclick="() => Counter++"></button>
191+
@code {
192+
public int Counter { get; set; } = 0;
193+
}
194+
```
195+
196+
The first tests uses auto refresh, set through the optional parameter `enableAutoRefresh` passed to FindAll:
197+
198+
```csharp
199+
public void AutoRefreshQueriesForNewElementsAutomatically()
200+
{
201+
var cut = RenderComponent<ClickAddsLi>();
202+
var liElements = cut.FindAll("li", enableAutoRefresh: true);
203+
liElements.Count.ShouldBe(0);
204+
205+
cut.Find("button").Click();
206+
207+
liElements.Count.ShouldBe(1);
208+
}
209+
```
210+
211+
The second test refreshes the collection manually through the `Refresh()` method on the collection:
212+
213+
```csharp
214+
public void RefreshQueriesForNewElements()
215+
{
216+
var cut = RenderComponent<ClickAddsLi>();
217+
var liElements = cut.FindAll("li");
218+
liElements.Count.ShouldBe(0);
219+
220+
cut.Find("button").Click();
221+
222+
liElements.Refresh(); // Refresh the collection
223+
liElements.Count.ShouldBe(1);
224+
}
225+
```
226+
227+
- **Custom exception when event handler is missing.** Attempting to triggering a event handler on an element which does not have an handler attached now throws a `MissingEventHandlerException` exception, instead of an `ArgumentException`.
228+
229+
### Deprecated
230+
- **`WaitForNextRender` has been deprecated (marked as obsolete)**, since the added `WaitForState` and `WaitForAssertion` provide a much better foundation to build stable tests on. The plan is to remove completely from the library with the final 1.0.0 release.
231+
232+
### Removed
233+
- **`AddMockHttp` and related helper methods have been removed.**
234+
The mocking of HTTPClient, supported through the [mockhttp](https://github.com/richardszalay/mockhttp) library, has been removed from the library. This was done because the library really shouldn't have a dependency on a 3. party mocking library. It adds maintenance overhead and uneeded dependencies to it.
235+
236+
If you are using mockhttp, you can easily add again to your testing project. See [TODO Guide to mocking HttpClient](#) in the docs to learn how.
237+
238+
### Fixed
239+
- **Wrong casing on keyboard event dispatch helpers.**
240+
The helper methods for the keyboard events was not probably cased, so that has been updated. E.g. from `Keypress(...)` to `KeyPress(...)`.

bunit.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A5D7B605-02D8-468C-9BDF-864CF93B12F9}"
77
ProjectSection(SolutionItems) = preProject
88
.editorconfig = .editorconfig
9+
CHANGELOG.md = CHANGELOG.md
910
Directory.Build.props = Directory.Build.props
1011
LICENSE = LICENSE
1112
README.md = README.md

src/Asserting/CollectionAssertExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics.CodeAnalysis;
43
using System.Linq;
54
using Xunit;
65
using Xunit.Sdk;

src/Asserting/DiffAssertExtensions.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics.CodeAnalysis;
4-
using System.Text;
5-
using System.Threading.Tasks;
6-
using AngleSharp;
73
using AngleSharp.Diffing.Core;
84
using Xunit;
95

src/Asserting/HtmlEqualException.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
4-
using System.IO;
54
using System.Linq;
65
using AngleSharp;
76
using AngleSharp.Diffing.Core;
8-
using AngleSharp.Dom;
97
using Bunit;
108

119
namespace Xunit.Sdk

src/Asserting/ShouldBeAdditionAssertExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Linq;
3-
using AngleSharp;
43
using AngleSharp.Diffing.Core;
54
using AngleSharp.Dom;
65
using Bunit.Diffing;

src/Asserting/ShouldBeRemovalAssertExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Linq;
3-
using AngleSharp;
43
using AngleSharp.Diffing.Core;
54
using AngleSharp.Dom;
65
using Bunit.Diffing;

src/Asserting/ShouldBeTextChangeAssertExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using AngleSharp;
54
using AngleSharp.Diffing.Core;
65
using AngleSharp.Dom;
76
using Bunit.Diffing;

0 commit comments

Comments
 (0)