This post is part of the third annual C# advent. Two new posts are published every day between 1st December and 25th December.
One significant area which has been lacking in Blazor is testing. There is a placeholder issue for it, but for a very long time, there wasn’t any progress. This is because testing was out of scope for the first official release of Blazor - which shipped with .NET Core 3 back in September.
However, just before the release in August, Steve Sanderson published a blog post introducing a prototype unit testing library for Blazor components. Steve’s prototype has the goal of getting the conversation going around testing in Blazor. I’ve wanted to test the library for a while, but I haven’t had the chance until now.
Let’s start by covering some of the high level questions about this prototype.
I want to stress this is a prototype library and there is ZERO support for it, there isn’t even a NuGet package right now. Therefore, anything you read here can, and most likely will, change.
What types of testing does it cover?
There are various ways to test web applications but the two most common are unit tests and end-to-end tests (E2E tests).
Unit testing
Unit tests are lightweight and fast to run if written correctly, of course. They test small “units” of code in isolation. For example, given a method which takes two numbers and returns the sum of them. You could write a unit test which checks that if you provide the inputs 2
and 2
that the method returns 4
. However, checks in isolation can also be their downfall. It’s possible that various units of code could run fine in isolation, but when put together they don’t quite match up, and errors can occur.
End to end testing
E2E tests can help to combat the issues with unit tests. You can test the whole application stack using E2E testing. E2E tests often use a headless browser to run tests which assert against the DOM. This is achieved using tools such as Selenium which drive a headless browser and provide an interface to access the HTML. The drawback of these type of tests is they’re much more heavyweight. They’re also known to be brittle and slow; it takes substantial effort to both make them reliable and keep them that way.
The best of both
What Steve’s prototype library attempts to do is to bring the best of both of these testing approaches but without the drawbacks - sounds pretty good to me!
How does it work?
Steve’s library supplies a TestHost
which allows components to be mounted using a TestRenderer
under the hood. It’s compatible with traditional unit testing frameworks such as XUnit and NUnit and gives us a straightforward and clean way of writing tests. This is an example of what a test looks like using XUnit.
<!-- MyComponent.razor -->
<h1>Testing is awesome!</h1>
public class MyComponentTests
{
private TestHost _host = new TestHost();
[Fact]
public void MyBlazingUnitTest()
{
var component = _host.AddComponent<MyComponent>();
Assert.Equal("Testing is awesome!", component.Find("h1").InnerText);
}
}
We use CSS selectors to locate points of interest in the rendered output. In the test above, I’m locating the h1
tag and asserting that it contains the text “Testing is awesome!”.
We can run this test like we would a traditional unit test using the Visual Studio Test Explorer.
The tests also run exactly how you would expect when using a CI pipeline. Here’s an example from Azure DevOps.
Using the library
As I mentioned earlier, there isn’t a NuGet package available for the library just yet. To use it you have to either download/clone/fork the repo from Steve’s GitHub account. I’ve created a fork of it to my GitHub, I’ve also updated all the packages to the latest versions.
Once you have a copy of the code, you can open up the solution included with the repo. You’ll find a sample application and some sample tests which give a few good examples of how to use the library.
You can play around with it from here if you just want to get to know it better. But I’ve wanted to get some tests added into my Blazored libraries for quite a while now. So I thought this would be the perfect opportunity to do that and see how the library works with a real-world project.
Testing Blazored Modal
We’re going to start by adding some tests to Blazored Modal. This is a reasonably straightforward component which is controlled via a service. By calling a method on the service and passing different options or parameters, the modal is displayed in different configurations.
I’ve decided on two test groupings, display tests and modal options tests. I like to try and group my tests to make them easier to find and maintain.
To start, we’ll add a copy of Steve’s testing library to the solution and also a new XUnit test project called Blazored.Modal.Tests
.
Next, we need to add a reference to the testing library from the XUnit project and a reference to the Blazored.Modal
project.
We’re going to create each of the test groupings I mentioned earlier as classes in the Blazored.Modal.Tests
project. But so we don’t have to duplicate boilerplate code we’re going to create a test base class to encapsulate it.
public class TestBase
{
protected TestHost _host;
protected IModalService _modalService;
public TestBase()
{
_host = new TestHost();
_modalService = new ModalService();
_host.AddService<IModalService>(_modalService);
}
}
We start by creating a new TestHost
instance, which is provided by Steve’s testing library. As the modal component relies on an IModalService
to function, we also need to add an instance of one to the TestHost
’s DI container. This is the place to replace any services with mocks if your components are using services which make external calls.
Now we have our TestBase
sorted, let’s get cracking with our display tests. Let’s start by making sure that the modals initial state is not visible.
public class DisplayTests : TestBase
{
[Fact]
public void ModalIsNotVisibleByDefault()
{
var component = _host.AddComponent<BlazoredModal>();
var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");
Assert.Null(modalContainer);
}
}
In the test we’re creating an instance of the BlazoredModal
component via the TestHost
. Once we have that instance, we can use it to look for particular state. In our case, we’re checking that no element has the .blazored-modal-active
CSS class, as it’s this class which makes the modal visible.
We now need a test to make sure the modal becomes visible when we call the Show
method on the IModalService
.
[Fact]
public void ModalIsVisibleWhenShowCalled()
{
var component = _host.AddComponent<BlazoredModal>();
_modalService.Show<TestComponent>("");
var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");
Assert.NotNull(modalContainer);
}
This time we’re using the _modalService
instance we setup in the TestBase
. Once we’ve created the instance of our BlazoredModal
component, we call the Show
method on the service. We then look for an element which has the .blazored-modal-active
CSS class and check it’s not null.
If you’re wondering about the TestComponent
type in the Show
call, it’s a simple component I created to use for these tests and looks like this.
internal class TestComponent : ComponentBase
{
public const string TitleText = "My Test Component";
[CascadingParameter] public ModalParameters ModalParameters { get; set; }
public string Title
{
get
{
var cascadedTitle = ModalParameters.TryGet<string>("Title");
return string.IsNullOrWhiteSpace(cascadedTitle) ? TitleText : cascadedTitle;
}
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(1, "h1");
builder.AddContent(2, Title);
builder.CloseElement();
}
}
We’ll see this used more later on.
So far so good, now we are going to test cancelling and closing the modal. Here are the tests.
[Fact]
public void ModalHidesWhenCloseCalled()
{
var component = _host.AddComponent<BlazoredModal>();
_modalService.Show<TestComponent>("");
var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");
Assert.NotNull(modalContainer);
_modalService.Close(ModalResult.Ok("Ok"));
modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");
Assert.Null(modalContainer);
}
[Fact]
public void ModalHidesWhenCancelCalled()
{
var component = _host.AddComponent<BlazoredModal>();
_modalService.Show<TestComponent>("");
var modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");
Assert.NotNull(modalContainer);
_modalService.Cancel();
modalContainer = component.Find(".blazored-modal-container.blazored-modal-active");
Assert.Null(modalContainer);
}
These two tests are very similar, the only difference is the method we call on the modal service, either Close
or Cancel
. As you can see, I’ve got two Assert
s in each test. The first one is asserting that the modal is visible, then, after the Cancel
or Close
methods are called, the second assert checks that it’s not anymore.
That’s it for the display tests. Let’s move on and look at the modal options tests next. I’m not going to go through them all as I think things will get very repetitive, but I do want to look at two of them.
public class ModalOptionsTests : TestBase
{
// Other tests omitted for brevity
[Fact]
public void ModalDisplaysCorrectContent()
{
var component = _host.AddComponent<BlazoredModal>();
_modalService.Show<TestComponent>("");
var content = component.Find("h1");
Assert.Equal(content.InnerText, TestComponent.TitleText);
}
[Fact]
public void ModalDisplaysCorrectContentWhenUsingModalParameters()
{
var testTitle = "Testing Components";
var parameters = new ModalParameters();
parameters.Add("Title", testTitle);
var component = _host.AddComponent<BlazoredModal>();
_modalService.Show<TestComponent>("", parameters);
var content = component.Find("h1");
Assert.Equal(content.InnerText, testTitle);
}
}
The first test is checking that the correct content gets rendered by the modal component. In the test, we’re checking that the content of the TestComponent
(see code from earlier) is rendered correctly inside the modal. The TestComponent
just contains a simple h1
tag which will display the string “My Test Component” by default.
The next test is checking that the component being displayed renders correctly based on a value passed using ModalParameters
, which are passed into child components via a CascadingParameter
. In this test, we’re setting a Title
parameter with the value “Testing Components”. We then check to make sure the correct title is displayed by the TestComponent
.
The reason I wanted to highlight these two tests is because they show the more E2E style of testing achievable with this library. In order to test the BlazoredModal
component fully we need to make sure it interacts with other components in the right way. And this is a very common scenario when building component based UIs, testing in isolation here wouldn’t give us the same amount of confidence as testing these components together.
You can view all the tests mentioned in this post at the Blazored Modal repo.
Summary
That’s where we’re going to leave things. We’ve managed to get setup with Steve’s library and write some decent tests to check the functionality of Blazored Modal.
In this post, we’ve taken a look at how to test Blazor components using Steve Sanderson’s prototype testing library. We looked at how to get up and running with the library before using it to write some tests for the Blazored Modal component library.
I hope you’ve found this post useful and if you have any feedback for Steve about his prototype, then please head over to the repo and open an issue.