Resilient testing approaches for Blazor components
I’ve been a fan of testing library when I was working with react about 2 years ago. Since then, I’ve started working on a project that’s using Blazor Web Assembly for front end. However, haven’t come across a similar framework for testing Blazor component that’s similar to testing library.
BUnit seems to be default testing framework for anything Blazor related. You would install it to your test project like any other nuget package installation.
dotnet add package bunit --version 1.12.6
The default project template for a Blazor Web Assembly project has a simple
<SurveyPrompty />
Blazor component which I will use to demonstrate writing
unit test in this blog post. For simplicity, lets pretend the implementation of
<SurveyPrompty />
component was…
<div class="alert alert-secondary mt-4">
<span class="oi oi-pencil me-2" aria-hidden="true"></span>
<strong>@Title</strong>
<span class="text-nowrap">
Please take our
<a target="_blank" class="font-weight-bold link-dark" href="#"
>brief survey</a
>
</span>
and tell us what you think.
</div>
@code { [Parameter] public string? Title { get; set; } }
The <SurveyPrompty />
component has 1 parameter called Title
which is then
rendered inside a <strong>
element, which is nested in <div />
with class
alert
. If we were to follow the BUnit documentation on how to write a test for the
<SurveyPrompty />
component, you’d end up with something similar to…
[Theory, InlineData("Foo"), InlineData("Bar"), InlineData("Baz")]
public void Render_Title(string title)
{
using var ctx = new TestContext();
var cut = ctx.RenderComponent<SurveyPrompt>(parameters =>
parameters.Add(p => p.Title, title));
var actual = cut.Find(".alert strong").TextContent;
Assert.Equal(title, actual);
}
The above test will pass, but there is 1 issue I have with this test. It couples
the styling, and structure of the component to the unit test. If the class
.alert
or the element <strong \>
were to be changed or replaced, the tests
would stop working.
The guiding principles at testing library is…
The more your tests resemble the way your software is used, the more confidence they can give you.
Test Id attribute approach
Testing library recommends avoiding using a test specific attribute, but
suggests that its way better than querying based on DOM structure or styling
css class names. It is common to use the attribute data-testid
as a means to
finding elements. I came across it first when using Cypress - End to end
testing framework. Cypress uses data-cy
, data-test
and data-testid
attributes. However, testing library seems to have a preference on
data-testid
attribute. They all serve the same purpose. You’d only use this
attribute strictly for finding elements for testing purposes and nothing else.
In our example, the <strong>
element surrounding the @Title
would have an
additional html attribute…
<strong data-testid="survey-prompt-title">@Title</strong>
With this approach, we can replace the Find()
method from previous test to
find by data-testid
attribute.
[Theory, InlineData("Foo"), InlineData("Bar"), InlineData("Baz")]
public void Render_Title_TestId(string title)
{
using var ctx = new TestContext();
var cut = ctx.RenderComponent<SurveyPrompt>(parameters =>
parameters.Add(p => p.Title, title));
var actual = cut.Find("[data-testid='survey-prompt-title']").TextContent;
Assert.Equal(title, actual);
}
Verify text content approach
This is the recommended approach according to the testing library, you’d try
to find if text passed into the Title
property was rendered by the component.
BUnit doesn’t have any direct support for this, but we can still acheive
this result by using the css selector :contains()
.
[Theory, InlineData("Foo"), InlineData("Bar"), InlineData("Baz")]
public void Render_Title(string title)
{
using var ctx = new TestContext();
var cut = ctx.RenderComponent<SurveyPrompt>(parameters =>
parameters.Add(p => p.Title, title));
cut.Find($":contains({title})");
}
Find()
will throw Bunit.ElementNotFoundException
exception when it can’t
find any elements that matches the contains()
css selector. Therefore, there
is no need for any more assertion statements. If the line doesn’t throw
exception, we’ve passed out test. We can clean this up further by creating an
extension method called VerifyTextContaining()
on IRenderedComponent
type.
public static class RenderedComponentExtensions
{
public static void VerifyTextContaining<T>(
this IRenderedComponent<T> component,
string text) where T : Microsoft.AspNetCore.Components.IComponent
=> component.Find($":contains({text})");
}
cut.VerifyTextContaining(title);
You can make the error message more useful than
Bunit.ElementNotFoundException : No elements were found that matches the selector ':contains(Foo)'
by catching the ElementNotFoundException
and using FluentAssertions
library or some other means.
Conclusion
Testing the behaviour rather than the structure of the elements within the
component is more resilient. Perhaps we should discourage the use of Find()
and FindAll()
methods all together in our BUnit test projects?
There is an open source project that I came across which has started implementing testing library APIs for Blazor and is called blazor-testing-library. Its fairly new and only has 2 stars at the time of writing this post.
Further reading
- Making your UI tests resilient to change by Kent C. Dodds