Unit Testing Blazor Components with bUnit and JustMock
What is bUnit?
bUnit is a testing library for Blazor components. And its goal is to make it easy to write comprehensive, stable unit tests.
You can:Setup and define components under tests in C# or Razor syntax
Verify outcome using semantic HTML comparer
Interact with and inspect components
Trigger event handlers
Provide cascading values
Inject services
Mock IJsRuntime
Perform snapshot testing
bUnit builds on top of existing unit testing frameworks such as xUnit, NUnit, and MSTest, which runs the Blazor components tests, just as any normal unit test.
Note:
Here are some of the links I think are useful and that I have been using to learn about this topic:
https://bunit.egilhansen.com/docs/getting-started/
https://www.telerik.com/blogs/unit-testing-blazor-components-bunit-justmock
https://www.telerik.com/blogs/unit-testing-blazor-components-bunit-justmock
using the link above I have come up with the following notes:
First, we will start with how to set up our projects and then continue with simple unit test examples.
Project Setup:
Let us begin setting up our projects. As with any other technology you will need at least two projects. The first one is the Blazor application and the second one is for the unit tests.
Creating the Blazor App Project
We can just use the premade visual studio template to create our Blazor project.
Our newly created project has the default pages and data classes. Which we will work on them for our examples.
Creating the Unit Test Project
We will then start creating our Unit test project by following the following steps down below:
As a first step create a Razor Class Library targeting .NET Core 3.1. The same version should be for the Blazor application as well.
The Link below shows us how to create a razor class library:
https://www.youtube.com/watch?v=BtXWj-yQF6c
Add the Microsoft.NET.Test.Sdk NuGet package to the project.
Add bUnit as a NuGet package to the project. Please notice that this package is still in beta and you will need to check the include prerelease checkbox. When trying to add the package you will notice the following error:
Add xUnit as a NuGet package to the project. I am using xUnit as I saw that it is supported by bUnit.
Add the xunit.runner.visualstudio NuGet package to the project. We need this package to execute the unit tests in Visual Studio.
Add the JustMock NuGet package
Note: JustMock is:
Telerik JustMock is an easy to use mocking tool designed to help you create better unit tests, faster than ever. JustMock makes it easier for you to create mock objects and set expectations independently of external dependencies like databases, web service calls, or proprietary code.
What Is Mocking and Why Do I Need It?
Mocking is a concept in unit testing where real objects are substituted with fake objects that imitate the behavior of the real ones. Mocking is done so that a test can focus on the code being tested and not on the behavior or state of external dependencies.
For example, if you have a data repository class that runs business logic and then saves information to a database, you want your unit test to focus on the business logic and not on the database. Mocking the “save” calls to your database ensures your tests run quickly and do not depend on the availability or state of your database. When you’re ready to make sure the “save” calls are working, then you’re moving on to integration testing. Unit tests should not cross system boundaries, but integration tests are allowed to cross boundaries and make sure everything works together (your code, your database, your web services, etc.).
Add a reference to the Blazor Demo application.
Build to validate there are no errors.
OK, we have created the test project and we successfully built it. Now we need to prepare our test class.
Add one class that will be used for the unit tests
Add usings to Bunit, Xunit and Telerik.JustMock
Make the class public
Inherit the ComponentTestFixture abstract class
Note:
I got into a bit of problem while trying to inherit from ComponentTestFixture.
Visual studio does not support this piece of code anymore. And it has been replaced by TestContext. So we are going to instead inherit from TestContext and then continue creating our unit test.
Now we are ready to start writing our first test.
The Simplest Unit Test
Running the Blazor Demo application that we have added takes us to a page which has three links for Home, Counter and Fetch Data. We Will start by testing out the functionality of the counter page where clicking a button will increase the counter.
------------------------------------------------------------------------------------------------------------
[Fact]
public void TestCounter()
{
// Arrange
var cut = RenderComponent<Counter>();
// Act
var element = cut.Find("button");
element.Click();
//Assert
cut.Find("p").MarkupMatches("<p>Current count: 1</p>");
}
------------------------------------------------------------------------------------------------------------
Note: While using the “ var cut = RenderComponent<Counter>();
“ keep in mind that we will get an error for Counter and it will be underlined with red color. In order to fix it highlight the word Counter and then click on :
“using AutomatedTesting.Paged;”.
Once the code above is written we can test to see if our test passes.
To test click on test in the top navigation bar and select Test explorer. You can get to the same page by pressing ctrl + E
After selecting Test Explored the following pop up will show up. Click on the green arrow to test the test case the counter test case. Once the test case passes you should be getting the following message
And now we can start writing test cases for the FetchData page. For this particular page we need to write two unit tests. The first should test that the loading text is shown when the forecasts variable is null. And the second one is to test that the data is shown correctly when there is actual data.
First Scenario:
Forecast in Null Scenario:
The first step for testing this scenario is registering the WeatherForecastService.Services.AddSingleton();
Note: AddSingleton is:
AddSingleton() - As the name implies, AddSingleton() method creates a Singleton service. A Singleton service is created when it is first requested. This same instance is then used by all the subsequent requests. ... A new instance of a Scoped service is created once per request within the scope.
text is shown. Here is how the unit test looks like:
------------------------------------------------------------------------------------------------------------
[Fact]
public void TestFetchData_NullForecast()
{
Services.AddSingleton<WeatherForecastService>();
var cut = RenderComponent<FetchData>();
// Assert that it renders the initial loading message
var initialExpectedHtml =
@"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
<p><em>Loading...</em></p>";
cut.MarkupMatches(initialExpectedHtml);
}
------------------------------------------------------------------------------------------------------------
Note:
Once you write the code shown above in your unit test class. We will get errors on “AddSingleton
” and “WeatherForecastService
”. In order to fix this, we just got to add the following two lines of code:
“using Microsoft.Extensions.DependencyInjection;”
“using AutomatedTesting.Data;”
Note:
There is another way to run the test case for a line of code. We can just click on the “references” line of code shown below [Fact] and click on run. This will automatically run the test cases for us.
If you execute this test now you will notice that it will fail. The reason for this is because the WeatherForecastService generates random values and the forecasts variable in the component will never be null.
This is a good candidate for mocking. If you are not familiar with the concept, in mocking, to test the required logic in isolation the external dependencies are replaced by closely controlled replacements objects that simulate the behavior of the real ones. For our scenario, the external dependency is the call to the WeatherForecastService.GetForecastAsync method. To mock this method, we will have to use a mocking framework like Telerik JustMock.
We then need to modify the WeatherForecastService to inherit an interface. We will work with that interface.
Add the following code:
------------------------------------------------------------------------------------------------------------
public interface IWeatherForecastService
{
Task<WeatherForecast[]> GetForecastAsync(DateTime startDate);
}
------------------------------------------------------------------------------------------------------------
Do not forget to that the WeatherForecastService should inherit this interface. Your code should look like the snippet shown below.
Next, the FetchData page should use it. Here is the chunk of code that should be modified:
Add the following line of code shown below:
@using AutomatedTesting.Data
@inject WeatherForecastService ForecastService
@inject IWeatherForecastService ForecastService
******************************************************************
Need to double check with Walter/Kajal/Yong, not sure if I did this part correctly
******************************************************************
Also, for the BlazorDemo App to continue working I need to modify the Startup.ConfigureServices method located in the startup page and add IWeatherForecastService as a singleton service with implementation WeatherForecastService. Like this:
services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
Now we are ready to create the mock. What we will do is create a mock of type IWeatherForecastService and arrange the GetForecastAsync method for any DateTime argument to return a value that will result in null value for the forecast variable. At the end, the mocked instance should be registered as implementation of our interface. Here is how the whole test looks:
------------------------------------------------------------------------------------------------------------
[Fact]
public void TestFetchData_ForecastIsNull()
{
// Arrange
var weatherForecastServiceMock = Mock.Create<IWeatherForecastService>();
Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny<DateTime>()))
.Returns(new TaskCompletionSource<WeatherForecast[]>().Task);
Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
// Act
var cut = RenderComponent<FetchData>();
// Assert - that it renders the initial loading message
var initialExpectedHtml =
@"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
<p><em>Loading...</em></p>";
cut.MarkupMatches(initialExpectedHtml);
}
------------------------------------------------------------------------------------------------------------
We will get an error for “TaskCompletionSource<WeatherForecast[]”. In order to fix the error, we need to add the following line of code to the top of the page.
Using System.Threading.Tasks;
If you run the test cases again, we should be getting the following result shown below.
Now, we can move on to the second scenario:
Forecast Has a Value
For this scenario we will create a mock of the GetForecastAsync similarly to what we did in the previous test, but this time the method will return a single predefined value. We will use this value later for validation.
Next, we will register the IWeatherForecastService with the implementation of the created mock. After that we will render the FetchData component. bUnit has an API that allows us to search for a nested component in another component. This is what we will do as we have already extracted the forecast data representation in another component. At the end we will compare the actual result with expected value. Here is what this unit test will look like:
------------------------------------------------------------------------------------------------------------
[Fact]
public void TestFetchData_PredefinedForecast()
{
// Arrange
var forecasts = new[] { new WeatherForecast { Date = DateTime.Now, Summary = "Testy", TemperatureC = 42 } };
var weatherForecastServiceMock = Mock.Create<IWeatherForecastService>();
Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny<DateTime>()))
.Returns(Task.FromResult<WeatherForecast[]>(forecasts));
Services.AddSingleton<IWeatherForecastService>(weatherForecastServiceMock);
// Act - render the FetchData component
var cut = RenderComponent<FetchData>();
var actualForcastDataTable = cut.FindComponent<ForecastDataTable>(); // find the component
// Assert
var expectedDataTable = RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
actualForcastDataTable.MarkupMatches(expectedDataTable.Markup);
}
------------------------------------------------------------------------------------------------------------
Note: Writing the code above will give an error for “ForecastDataTable”. To fix this error we need to do couple of things.
First, we need to right click on Pages ->Add -> razor components
Name it ForecastDataTable.razor
Once we create the page, we then need to write down the following code.
------------------------------------------------------------------------------------------------------------
@using AutomatedTesting.Data
<table class="forcast-data-table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in Forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
@code {
private WeatherForecast[] _forecasts = Array.Empty<WeatherForecast>();
[Parameter]
public WeatherForecast[] Forecasts
{
get => _forecasts;
set => _forecasts = value ?? Array.Empty<WeatherForecast>();
}
}
------------------------------------------------------------------------------------------------------------
Note: We cannot do this final test due to us having to use Telerik until the end of the demo.