Introduction to the
The introduction of Blazor in C# development gives developers the ability to extend their development to browsers without relying on traditional JavaScript frameworks like React, vue.js, and Angular.
While it is easier to set up tests in a traditional JavaScript framework, Blazor needs to put some tools and packages together and then understand how and what to test in your application.
This article will show you how to set up tests for a simple Blazor counter application and extend it to include almost everything a C# developer might want to test in a Blazor application.
Set up a test environment
First, let’s set up the demo project.
Create a new project
In Visual Studio, click New.
From the Web and console menus, select Applications, and then Select Blazor Server Application.
On the next page, continue without validation, and then set the project name and solution name. Click Create.
Set up a test project
To set up a test project, click New Solution from the drop-down menu of the file menu; A template window should pop up.
From the Web and Console groups in the left sidebar, select Tests, select the xUnit test project, and click Next.
Use the same version of the framework as your main project, then click Next.
Finally, set a name for the solution and project, and click Create.
Once completed, your Visual Studio should have the following sidebar.
Link the main project to the test project
To enable the test project to reference and use the main project, we must create a link in the test project so that we can import and use components, classes, and interfaces from the main project.
In Visual Studio, right-click test solution from the left sidebar, select Edit Project file, and add
Setting test dependencies
Install the bUnit
From the project menu, click Manage NuGet packages, search bUnit, select bUnit and bunit.core, click Add Packages, select both solutions, and then click OK.
Install the xUnit
The test project is booted as the xUnit project. By default, it comes with the xUnit package.
Install behind Moq
Moq is an assertion library that is useful for testing whether the expected results match the returned results.
We can install Moq in the same way we installed bUnit. Simply search and select Moq, click Add Package, select test project, and then click OK.
With the bUnit test
XUnit is a testing framework that provides an interface to run Blazor applications outside of the browser and still interact with the output through code.
BUnit is an interface through which we can interact with Blazor components. The interface provided by bUnit makes it possible to fire events on a Blazor component, find elements on the component, and make assertions.
The test set
To test Blazor applications with bUnit, the test suite must have a test case capability in one of the classes in the test project.
The code in the test case should have the following.
Arrange
, set aTestContext
A virtual environment for rendering Blazor components.Act
Render a component into the test environment, trigger actions, and make network requests.Assert
Check that the event is fired and that the correct text is displayed.
As an example, the following Settings illustrate the above steps.
using BlazorApp.Pages; using Bunit; using Xunit; namespace BlazorAppTests { public class CounterTest { [Fact] public void RendersSuccessfully() { using var ctx = new TestContext(); // Render Counter component. var component = ctx.RenderComponent<Counter>(); // Assert: first, find the parent_name vital element, then verify its content. Assert.Equal("Click me", component.Find($".btn").TextContent); }}}Copy the code
From the right sidebar, click Tests, and then click Run All to run the test.
Pass parameters to the component
Sometimes, components need parameters to render correctly. BUnit provides an interface to handle this problem.
First, let’s modify the Counter component in our application solution to look like this.
@page "/counter/{DefaultCount:int? }" <h1>Counter</h1> <p>Current count: <span id="counterVal">@currentCount</span></p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; [Parameter] public int DefaultCount { get; set; } protected override void OnParametersSet() { if (DefaultCount ! = 0) { currentCount = DefaultCount; } } private void IncrementCount() { currentCount++; }}Copy the code
First, notice how we updated the path to accept a DefaultCount argument, which is an integer. ? Tell Blazor that this parameter is optional and not required for the component to run.
Next, notice that the DefaultCount attribute in the C# code has a [Parameter] attribute. We have suspended the OnParametersSet lifecycle method to notify the component when the parameter is set. This ensures that we use it to update the component currentValue property, rather than having the component count from zero.
We can render this component in the bUnit test case using the following methods.
using BlazorApp.Pages; using Bunit; using Xunit; namespace BlazorAppTests { public class CounterTest { public void RendersSuccessfully() { using var ctx = new TestContext(); Action onBtnClickHandler = () => { }; // Render Counter component. var component = ctx.RenderComponent<Counter>( parameters => parameters // Add parameters .Add(c => c.DefaultCount, 10) .Add(c => c.OnBtnClick, onBtnClickHandler) ); // Assert: first find the parent_name strong element, then verify its content. Assert.Equal("Click me", component.Find($".btn").TextContent); }}}Copy the code
In line 14 of the above test, we rendered the component and then passed a callback to the component, calling (p =>); .
We then Add the Add method to the argument (p => p.dd (c => c.faultCount, 10); To set the parameter to 10.
We can use the same method to pass an event callback, p.ad (c => C.nbtnClickHandler, onBtnClickHandler). Thus, we increment the counter in the onBtnClickHandler action, not in the Counter component.
Pass the input and service to the component
Some components rely on external services to run, while others rely on external fields. We can do this with bUnit by testing the services.addSingleton method in the context.
In the counter application shown here, there is a fetchData.Razor file that relies heavily on a WeatherForecastService service. Let’s try running this file in an xUnit test project.
Create a new file called FetchDataTest. Cs in your test project and add the following.
using System; using BlazorApp.Data; using BlazorApp.Pages; using Bunit; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace BlazorAppTests { public class FetchDataTest { [Fact] public void RendersSuccessfully() { using var ctx = new TestContext(); ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService()); // Render Counter component. var component = ctx.RenderComponent<FetchData>(); Assert.Equal("Weather forecast", component.Find($"h1").TextContent); }}}Copy the code
Notice how we use the AddSingleton interface to add a new service to our test runner context. When we run the test file, we should get a successful result.
The event
Above, we saw how to set the callback for an event within the test case component. Let’s look at how to fire an event on an element within a component.
The counter test file has a button that increases the counter when clicked. Let’s test it out and make sure we can click the button and see the count update on the page.
In the countertest.cs file in the test project, add the following test cases to the CounterTest test suite class.
[Fact]
public void ButtonClickAndUpdatesCount()
{
// Arrange
using var ctx = new TestContext();
var component = ctx.RenderComponent<Counter>();
// Render
var counterValue = "0";
Assert.Equal(counterValue, component.Find($"#counterVal").TextContent);
counterValue = "1";
var buttonElement = component.Find("button");
buttonElement.Click();
Assert.Equal(counterValue, component.Find($"#counterVal").TextContent);
}
Copy the code
This component is set up in the Arrange section. As usual, in the “Render “section, we first assert that the component starts from zero.
We then use the test context component’s. The Find interface, which gets a reference to a button and then returns a reference to an element, also has some apis like the Click() method.
Finally, we assert the value of the component to confirm that a button click will do the same.
Waiting for asynchronous status updates
Note that we did not test to see if any data was rendered after the service was injected. Just like the FetchData.Razor component, some components take time to render the correct data.
We can wait for an asynchronous state update through the component.waitForState(fn, duration) method.
[Fact]
public void RendersServiceDataSuccessfully()
{
using var ctx = new TestContext();
ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());
// Render Counter component.
var component = ctx.RenderComponent<FetchData>();
component.WaitForState(() => component.Find(".date").TextContent == "Date");
Assert.Equal("TABLE", component.Find($".table").NodeName);
}
Copy the code
The above example waits for asynchronous data to load until the anonymous function in WaitForState is called, which tests to find an element with the date class. Once found, we can make some further claims about the results.
Authentication token
We can also verify that a component’s tags follow the same pattern through the MarkupMatches bUnit interface method.
For example, we can test if the index contains “Hello, World!” H1 for text content.
First, create a new file in the test project, name it Indextest.cs, and add the following.
using System; using BlazorApp.Pages; using Bunit; using Xunit; namespace BlazorAppTests { public class IndexTest { [Fact] public void RendersSuccessfully() { using var ctx = new TestContext(); // Act var component = ctx.RenderComponent<BlazorApp.Pages.Index>(); // Assert Assert.Equal("Hello, world!" , component.Find($"h1").TextContent); }}}Copy the code
In addition, we can also pass. Find (we’re already doing this), and FindAll, to verify that a component contains an element that returns all characteristics that match the query. These methods use csS-like selectors, which make it easier to traverse nodes.
Mock IJSRuntime
IJSRuntime is an interface that enables the Net code to interact with JavaScript is possible.
Some components may depend on it; For example, a component can make API calls using jQuery methods.
If we have a JavaScript function getPageTitle in our project, we can simulate the call to that function so that anywhere in our component, the result will be what we might specify in our test case.
using var ctx = new TestContext();
ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());
var theResult = "some result";
ctx.JSInterop.Setup<string>("getPageTitme").SetResult(theResult);
// Render Counter component.
var component = ctx.RenderComponent<FetchData>();
Assert.Equal(theResult, component.Find($".page-title").TextContent);
Copy the code
Mock HttpClient
Some applications rely on data from remote servers to run properly.
Part of the strategy for unit testing is to keep the dependencies of each test case intact. Relying on an HTTP client to touch a remote server’s component to render a function, if the result is not static, would break our tests.
We can eliminate this problem by emulating HTTPClient, a library that can make HTTP requests from inside Blazor applications to the outside world.
According to bUnit’s documentation, bUnit does not include this feature by default, but we can rely on third-party libraries to implement this feature.
First, add the RichardSzalay.mockhttp package to the test project.
Dotnet add package Richardszalay. MockHttp --version 6.0.0Copy the code
Next, in the root systems of test project to create a file called MockHttpClientBunitHelpers, and add the following content.
using Bunit; using Microsoft.Extensions.DependencyInjection; using RichardSzalay.MockHttp; using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; public static class MockHttpClientBunitHelpers { public static MockHttpMessageHandler AddMockHttpClient(this TestServiceProvider services) { var mockHttpHandler = new MockHttpMessageHandler(); var httpClient = mockHttpHandler.ToHttpClient(); httpClient.BaseAddress = new Uri("http://localhost"); services.AddSingleton<HttpClient>(httpClient); return mockHttpHandler; } public static MockedRequest RespondJson<T>(this MockedRequest request, T content) { request.Respond(req => { var response = new HttpResponseMessage(HttpStatusCode.OK); response.Content = new StringContent(JsonSerializer.Serialize(content)); response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return response; }); return request; } public static MockedRequest RespondJson<T>(this MockedRequest request, Func<T> contentProvider) { request.Respond(req => { var response = new HttpResponseMessage(HttpStatusCode.OK); response.Content = new StringContent(JsonSerializer.Serialize(contentProvider())); response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return response; }); return request; }}Copy the code
Now create a new test case and add the following.
[Fact]
public void FetchResultTest()
{
var serverTime = "1632114204";
using var ctx = new TestContext();
var mock = ctx.Services.AddMockHttpClient();
mock.When("/getTime").RespondJson<string>(serverTime);
// Render Counter component.
var component = ctx.RenderComponent<FetchData>();
Assert.Equal(serverTime, component.Find($".time").TextContent);
}
Copy the code
Here, we declare a variable, used to hold our expectations of the server, and then through a bUnit auxiliary methods CTX. Services. AddMockHttpClient, to simulate the client added to the context service, This method will find MockHttpClientBunitHelpers, and turn it into context.
We then use mock references to simulate the response we expect from the route. Finally, we assert that part of our component has the value we returned from the mock request.
conclusion
In this article, we saw how to set up a Blazor project and add another xUnit test project. We also used bUnit as a testing framework and discussed using bUnit to test Blazor components.
In addition to xUnit as a testing framework, bUnit can also be run using similar concepts and apis in the nUnit testing framework.
In this article, we introduced the general usage of bUnit. Advanced usage can be found on the bUnit documentation website.
The postTesting in Blazor: The Complete Tutorial first appeared on The LogRocket blog.