Unleashing the Power of Unit Testing: Supercharge Blazor Server Async Components' UI Behavior with .NET Core and BUnit
Matteo Maggiolini
Posted on June 25, 2023
Hi folks,
We are all familiar with unit testing frameworks like Angular, React, and Vue on the client side. But what if we want to test the UI of hybrid or server-side frameworks like Blazor?
Blazor, the web framework by Microsoft, has gained popularity for building interactive web applications using C#. With the release of .NET 8, Blazor has become even more powerful.
Testing is a crucial part of software development, and BUnit, a testing framework, makes it easy to write unit tests for Blazor components. In this blog post, we will explore how to use BUnit (and of course NUnit) to test button clicks in a server-side Blazor component.
Assuming that you are already familiar with Blazor server-side projects
Let's consider a component similar to the one provided, incorporating asynchronous streaming rendering capabilities to enhance complexity. Additionally, it utilizes JS interop for interaction, dynamically rendering buttons, and so on...
@using Microsoft.AspNetCore.Components
@inject IJSRuntime JSRuntime
<h3>Test Component</h3>
<p>Click the button to call a server action and display data:</p>
@if (dataList != null && dataList.Any())
{
<h4>Data List</h4>
<ul>
@foreach (var item in dataList)
{
<li>
@item
<button class="server-action-button" @onclick="() => CallServerAction(item)">Call Server Action</button>
</li>
}
</ul>
}
@code {
private List<string> dataList;
private async Task CallServerAction(string item)
{
await Task.Delay(1000);
dataList.Add(item + " (Added on server)");
await JSRuntime.InvokeVoidAsync("myCustomJsFunction", "Server action completed!");
await InvokeAsync(StateHasChanged);
}
protected override void OnInitialized()
{
dataList = new List<string> { "Item 1", "Item 2", "Item 3" };
}
}
Now, we can test the button click behavior in a simple and straightforward way
[Test]
public void Test_ButtonClick()
{
using var ctx = new TestContext();
var jsInteropMock = new Mock<IJSRuntime>();
var jsRuntime = jsInteropMock.Object;
ctx.Services.AddSingleton(jsRuntime);
var component = ctx.RenderComponent<TestComponent>();
component.WaitForState(() => component.Instance.dataList != null, TimeSpan.FromSeconds(10));
component.Render();
var button = component.FindAll("button.server-action-button")[0];
var serializeObject = JsonConvert.SerializeObject(component.Instance.dataList);
Assert.DoesNotThrow(() => button.Click());
component.WaitForState(() =>
{
var serializeObjectToCheck = JsonConvert.SerializeObject(component.Instance.dataList);
return !serializeObjectToCheck.Equals(serializeObject);
}, TimeSpan.FromSeconds(30));
Assert.Pass("data has changed successfully.");
}
The test case sets up a *TestContext *(Bunit.TestContext, the factory that makes it possible to create components under tests)
A mock instance of IJSRuntime is created using Mock and assigned to jsRuntime. It is then added to the services collection of the TestContext using ctx.Services.AddSingleton(jsRuntime). This allows us to provide a mock implementation of IJSRuntime to the component.
Of course, you will understand that in this context, you can add all the necessary injected services and, if needed, mock them for testing purposes.
The TestComponent is rendered by calling ctx.RenderComponent(). The WaitForState method is used to wait until the initial state of the dataList is not null before rendering the component.
component.Render() is called explicitly to render the component.
The button element with the CSS class "server-action-button" is found using component.FindAll("button.server-action-button")...
The button is simulated to be clicked by calling button.Click().
The test case waits for the state to change using component.WaitForState(). It provides a condition that checks if the dataList has been updated by comparing the serialized dataList before and after the button click.
Of course, we can see that the testing assertions used here are for demonstration purposes only. In reality, you have the flexibility to perform various types of checks and validations, including verifying UI state changes, user interactions, and much more. The possibilities are virtually limitless when it comes to testing and ensuring the desired behavior of your Blazor components.
That's all Folks!
IMHO the Automated testing in Blazor opens up a world of possibilities for ensuring the functionality and reliability of real user interfaces. The example we discussed represents a simple case scenario where we tested the button click behavior and state update in a Blazor component.
By utilizing BUnit and NUnit, we were able to simulate user interactions, trigger server actions, and validate the resulting changes in the component's state. This demonstrates the potential of automated testing in Blazor to catch bugs, validate expected behavior, and provide confidence in the quality of the application.
I think this is a good point to start from
Conclusion
The flexibility and extensibility of automated testing allow you to test various scenarios and handle complex interactions within Blazor components. From validating UI elements to testing complex user workflows, the only limit is your imagination and the specific requirements of your application.
Automated testing in Blazor not only saves time and effort in manual testing but also provides a safety net for detecting issues early in the development cycle. It enables developers to iterate quickly, make changes confidently, and deliver a high-quality user experience.
So, unleash your creativity and explore the full potential of automated testing to ensure that your Blazor applications are robust, reliable, and provide an exceptional user experience.
See you for the next Blazor and .NET post!
Stay tuned
Posted on June 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.