Test driven development on BLAZOR applications

By Joshua Holden



Summary

If you are reading this you probably already know what TDD is, but for the uninitiated, test-driven development is a development approach where you first start with a bunch of test cases such as: "If I click Add post button then a post is saved to the database", then taking a test case you then wire up a test, once it has failed, which it will because no code will have been written to perform the action to be tested, you then add in the code to make the test case pass and refactor it until you are happy with it.

This approach firstly saves money and time in the long run as code will be less bug-prone owing to the fact that most test cases have already been accounted for and it allows you to approach writing code in a more methodical and planned fashion and encourages loosely coupled code to be written which is great for solid code, secondly, if down the line further code is added which could potentially break existing functionality (introduce regression issues) then the unit tests will fail instantly alerting the developer the change they have made causes issues which need address. There are many more reasons to use TDD when working on a software solution but the aforementioned cases are probably the most useful.

In this blog post, I will cover bunit testing on a BLAZOR application, you can also watch a video below, covering setup and usage of this library.

Setup

Once you have fired up visual studio and have a BLAZOR application set up and ready to go the first thing to do is to add a new xUnit testing project to your solution,  to do this right-click on your solution, click "Add" then "New solution", choose "xUnit Test Project"

Now you have added the xUnit project to your solution you will need to reference your main Blazor application from the test project, to do this right click on "Dependencies" on  the unit test solution then click add project reference:

You will then need to reference the main BLAZOR project by ticking the checkbox then clicking OK:


Finally, you need to add the Nuget packages, bunit,bunit.xunit and MOQ

And you are now done, it's time to start writing some tests.

Writing tests

In order to better explain what bUnit allows you to do, I'm going to post below the page markup for the Counter page  and the tests that run against it then break down beneath the individual tests and what's happening:

Counter razor page

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<p>You are @(IsAuthorised == true ? "Authorised" : "Unauthorised") </p>

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthState { get; set; }
    private System.Security.Claims.ClaimsPrincipal User;
    private bool IsAuthorised;

    protected override async Task OnInitializedAsync()
    {
        User = (await AuthState).User;
        if (User.Identity.IsAuthenticated)
        {
            IsAuthorised = true;
        }
    }

    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

Counter test code

public class CounterTests : TestContext
    {
        TestContext tc = new TestContext();
        TestAuthorizationContext AuthContext;

        public CounterTests()
        {
            AuthContext = tc.AddTestAuthorization();
        }
        [Fact]
        public void AssertOnLoadPageCountIs0()
        {
           var page = tc.RenderComponent<CascadingAuthenticationState>(x=>x.AddChildContent<Counter>());
            page.Find("p").MarkupMatches("<p>Current count: 0</p>");
        }
        [Fact]
        public void ConfirmSingleClickIncrements()
        {
            var page = tc.RenderComponent<CascadingAuthenticationState>(x => x.AddChildContent<Counter>());
            page.Find("button").Click();
            page.Find("p").MarkupMatches("<p>Current count: 1</p>");
        }
        [Fact]
        public void LoggedIn()
        {
            AuthContext.SetAuthorized("sss.xxs@dss.xom", AuthorizationState.Authorized);
            var page = tc.RenderComponent<CascadingAuthenticationState>(x => x.AddChildContent<Counter>());

            page.FindAll("p")[1].MarkupMatches("<p>You are Authorised</p>");
        }
    }

Now you have had a chance to scan the code and get a feel for how it all hangs together, I will now attempt to break down each test  and give a summary of how it works, starting with the first test to verify when the counter page loads it displays 0 for the amount of times clicked:

[Fact]
        public void ConfirmZeroClicksOnLoad()
        { 
           var page =  tc.RenderComponent<CascadingAuthenticationState>(x=> x.AddChildContent<Counter>());
            page.Find("p").MarkupMatches("<p>Current count: 0</p>");
        }

The first thing to note is the [Fact] attribute, this is to flag to xunit that the test is parameterless for testing invariants in your code, if your methods contain parameters then you would need to use the [Theory] attribute with inline data for the parameters, an example of this is shown below:

[Theory]
[InlineData(1, 2,3)]
[InlineData(8,10,18)]
public void DoesAddCorrectly(int val1, int val2, int output)
{
    var Addition = Adder()
    var ret= Adder.Add(value1, value2);
    Assert.Equal(output, ret);
}

The theory attribute is therefore great for testing methods that take parameters to ensure output is as expected.
The second thing to note is the counter page is using cascading parameters for authorisation, if the page did not have authorisation then you could load a page instead using the following code:

var page =  tc.RenderComponent<Counter>();

The RendererComponent method takes the component name for example here the "Counter.Razor" component and loads it as a browser would, exposing the rendered HTML, below I show a screenshot showing the output of a rendered page:

The results contain a list of all of the nodes on the page as well as the raw markup and provide a range of methods for working with objects on the page and verifying markup meets expectations.

The final line of this test, first of all calls the Find method which takes CSS selectors to locate the first p element, then verifies the markup matches expected, in this instance:

 

<p>Current count: 0</p>

The next test is pretty much the same but in this one, the button is found on the page and clicked, the HTML is then checked  to verify the count is now at 1:

[Fact]
        public void ConfirmOneClickGivesOneCount()
        {
            var page = tc.RenderComponent<CascadingAuthenticationState>(x => x.AddChildContent<Counter>());
            page.Find("button").Click();
            page.Find("p").MarkupMatches("<p>Current count: 1</p>");
        }

This is done by finding the button and using the Click() method shown above.

The final test on this page is slightly more complicated as it involves creating a test double for the user principle and injecting the authorisation context into the page  render flow, this is useful for verifying users can only  see components and areas they should.

 [Fact]
        public void LoggedInCheck()
        {
            TestContext tc = new TestContext();
            TestAuthorizationContext AuthContext = tc.AddTestAuthorization();
            AuthContext.SetAuthorized("sffsdf.fsffe.com", AuthorizationState.Authorized);
            var page = tc.RenderComponent<CascadingAuthenticationState>(x => x.AddChildContent<Counter>());

            page.FindAll("p")[1].MarkupMatches("<p>You are authorised</p>");
        }

If you have a more complex application that utilises user roles then the TestAuthorisationContext can also deal with roles as shown below:

AuthContext.SetRoles("Moderator");

The final part of unit I want to discuss in this post is mocking and injecting services into a page, take for example the below FetchData page:

@page "/fetchdata"

@using BlazorTDDDemo.Data
@inject IWeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="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;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync();
    }
}

Looking at the Razor markup for this page, you can see at the top it injects in a service and then furtherdown uses that service to retrieve weather forecasts, we can also handle this in unit using MOQ to mock up a fake service then inject it into the page, an example of doing this is shown below:

public class FetchTests : TestContext
    {
        TestContext tc = new TestContext();

        [Fact]
        public void ConfirmServiceLoadsAndDisplaysAResult()
        {
            var service = new Mock<IWeatherForecastService>();
            service.Setup(s => s.GetForecastAsync()).Returns(Task.FromResult(new[] { new WeatherForecast { Date = DateTime.Now, Summary = "Bracing", TemperatureC = 12 } }));
            tc.Services.AddSingleton<IWeatherForecastService>(service.Object);
            var page = tc.RenderComponent<FetchData>();
            page.Find("p").MarkupMatches("<p>Current count: 0</p>");
        }


    }

bunit shown here allows the TestContext to take a version of the mocked WeatherForecastService and add it into the dependency container so it can be injected into the component and render time.

While this post covers the majority of use cases for bunit there is much more to it but currently, as the library is still in beta status there is not a great amount of documentation, however its definitely worth your time to take a look at the bunit page to get a greater feel for what it is capable of by clicking here.

Thanks for taking the time to read this and happy coding!

Comments


Comments are closed