Implementing a Mock-Driven Approach in Blazor Applications

Implementing a Mock-Driven Approach in Blazor Applications

When starting a project, you typically work with clients on UI sketches. The client provides a list of requirements or pages they want to see. You begin building the client app and backend, designing the database schema, etc. After a few weeks, you present the first demo to the client, only to discover they want to change everything.

Does that sound familiar to you?

Using a "UI first" or "mock-driven approach" can indeed save time for both you and your client, particularly when the client is uncertain about the final product. This approach allows for more efficient development, enabling you to better accommodate changes requested by your client.

This article discusses the "UI first" approach in developing a Blazor application. By building a page and using dependency injection to pass a reference to a service, we will create a mock class for the service. We will then replace the mock with a real service implementation, keeping web API calls abstracted and creating a mock there until the backend is ready. This tutorial will show you how to set up a Visual Studio solution optimally.

A mock driven approach is useful for developing applications with multiple layers of logic. These layers typically include presentation (UI), data transformation and state management (services), and communication with the server (network). By separating and abstracting each layer, we can achieve polymorphism, which allows for more flexibility in implementation.

To implement a solution gradually, we can start with the UI layer and define the interface for the service. Instead of implementing the actual service, we can create a mock class that acts as the real interface for now. This enables us to focus on the UI without worrying about middleware logic. As we refine the service interface, we ensure that our page receives all the necessary information.

The mock driven approach allows for incremental development and testing of individual layers before fully integrating them into the complete solution. This approach can lead to more maintainable and robust applications.

Step 1: Create the Blazor app and divide it into three layers.

Let's start by creating a Blazor WebAssembly App:

The full source of this solution is available on GitHub.

After creating the project, reorganize it into a more structured format by dividing the client project into three parts:

  • Client/UI: The original Blazor WASM project containing components, pages, etc.

  • Services: A Class Library containing all the logic related to processing requests and responses from the server, including caching, managing state, and data presentation. The service's library is just a regular Class Library:

  • Network: Another Class Library responsible for handling REST calls.

The Client Blazor app should depend on the Services library, while the Services library should depend on the Network library. This establishes a clear hierarchy among the components. We will configure dependency injection (DI) for both services and web APIs at a later stage, after creating the mocks.

Step 2: Create and configure the “mock” project.

Create a "Mocks" Class Library project and add it to the solution. Your Solution Explorer should look like this:

We don't want the Mocks to be built in all project configurations. Ensure that Debug and Release configurations don't build Mocks by unchecking the Mocks project in both configuration options:

To build mocks, create two new configurations: MockService and MockNetApi:

Configure the main project's dependencies by making the Mocks dependency conditional:

<ItemGroup>
  <ProjectReference Include="..\Net\MyBlazorApp.Net.csproj" />
  <ProjectReference Include="..\Services\MyBlazorApp.Services.csproj" />
  <ProjectReference Include="..\..\Shared\MyBlazorApp.Shared.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='MockNetApi'">
  <ProjectReference Include="..\Mocks\MyBlazorApp.Mocks.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='MockService'">
  <ProjectReference Include="..\Mocks\MyBlazorApp.Mocks.csproj" />
</ItemGroup>

In addition to the solution build configuration, ensure the correct DI behavior. All three projects (Services, Network, and Mocks) should contain service extension methods that are called in Program.cs depending on the selected configuration:

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
//...
#if MOCKNETAPI
        builder.Services.AddMockNetApi();
#else
        builder.Services.AddNetApi();
#endif

#if MOCKSERVICE
        builder.Services.AddMockAppServices();
#else
        builder.Services.AddAppServices();
#endif
//...
    }
}

Step 3: Create a page and define a service interface.

Instead of creating a new page, we are going to use the FetchData.razor (a part of the project template). Since this page displays a Weather forecast, we are going to modify it to rely on IWeatherForecastService to obtain the data. Additionally, we will add a special feature to this service: it will generate a summary or description of the forecast based on the temperature. To accommodate this field, we will introduce a local model class, WeatherForecastItem, in the Services project:

using MyBlazorApp.Shared;

namespace MyBlazorApp.Services.Model;

public class WeatherForecastItem : WeatherForecast
{
    public string? Summary { get; set; }
}

IWeatherForecastService itself will be part of the Services project in the Api folder:

namespace MyBlazorApp.Services.Api;

public interface IWeatherForecastService
{
    Task<WeatherForecastItem[]> GetForecastAsync();
}

Now, we will remove the reference to HttpClient and add IWeatherForecastService to the page:

@page "/fetchdata"
@using MyBlazorApp.Services.Api;
@using MyBlazorApp.Shared
@inject IWeatherForecastService Service

//...

@code {
    private WeatherForecastItem[]? forecasts;

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

Step 4: Implement a mock for this service.

If you start the application now and click on the "Fetch data" link in the menu on the left side, you will see an error in the browser's console: Unhandled exception rendering component: Cannot provide a value for property 'Service' on type 'MyBlazorApp.Client.Pages.FetchData'. There is no registered service of type 'MyBlazorApp.Services.Api.IWeatherForecastService'.

This is the time to introduce the mock class:

using MyBlazorApp.Services.Api;
using MyBlazorApp.Services.Model;

namespace MyBlazorApp.Mocks.ServicesMocks;

internal class WeatherForecastServiceMock : IWeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public async Task<WeatherForecastItem[]> GetForecastAsync()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecastItem
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToArray();
    }
}

Next, register WeatherForecastServiceMock in the DI services. Add the following line in ServiceExtensions.cs:

public static IServiceCollection AddMockAppServices(this IServiceCollection services)
{
     services.AddScoped<IWeatherForecastService, WeatherForecastServiceMock>();

    return services;
}

Now, switch to the "MockService" configuration, build, and run.

Step 5: Repeat steps 3 and 4 until the application's UI is ready.

Add more pages, service interfaces, or methods to the existing services. Iterate and refine until you have something you can demo to your client.

Step 6: Demo it to the client.

During the demonstration, it is important to let the client know that it is only a prototype and most of the work still needs to be done.

Good luck with your demo!

Final Step (a big one): Implement the service itself, web API interfaces, and unit tests.

Once the UI part and service interfaces are defined, you can proceed with the service implementation.

using MyBlazorApp.Net.Api;
using MyBlazorApp.Services.Api;
using MyBlazorApp.Services.Model;

namespace MyBlazorApp.Services;

internal class WeatherForecastService : IWeatherForecastService
{
    IWeatherForecastApi Api { get; set; }
    public WeatherForecastService(IWeatherForecastApi api)
    {
        Api = api;
    }

    private static string FindSummary(int temperatureC)
    {
    //...
    }

    public async Task<WeatherForecastItem[]> GetForecastAsync()
    {
        var result = await Api.GetForecastAsync();
        return result.Select(x => new WeatherForecastItem
        {
            Date = x.Date,
            TemperatureC = x.TemperatureC,
            Summary = FindSummary(x.TemperatureC)
        }).ToArray();
    }
}

and add it to the DI in ServiceExtensions.cs:

public static class ServiceExtensions
{
    public static IServiceCollection AddAppServices(this IServiceCollection services)
    {
        services.AddScoped<IWeatherForecastService, WeatherForecastService>();

        return services;
    }
}

This article could end here since it covers the most important part with UI mocks. However, I want to give a complete picture of this project structure, which is why I introduced the Network layer earlier. First, it allows you to build and test the service's logic without having an actual back-end ready. It also provides the ability to cover the service with unit tests by mocking the network layer.

Mocking web API follows the same principle as with services. We are going to create IWeatherForecastApi in the Network project in the Api folder:

using MyBlazorApp.Shared;

namespace MyBlazorApp.Net.Api;

public interface IWeatherForecastApi
{
    Task<WeatherForecast[]> GetForecastAsync();
}

and we are going to create a mock for it in the Mock project:

using MyBlazorApp.Net.Api;
using MyBlazorApp.Shared;

namespace MyBlazorApp.Mocks.NetApiMocks;

internal class WeatherForecastApiMock : IWeatherForecastApi
{
    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
        }).ToArray();
    }
}

and register it in ServiceExtensions.cs:

public static IServiceCollection AddMockNetApi(this IServiceCollection services)
{
    services.AddScoped<IWeatherForecastApi, WeatherForecastApiMock>();

    return services;
}

Closing Thoughts

Thank you for taking the time to read this article! I hope it proves useful for your ongoing projects. Your feedback and shared experiences are invaluable, so please feel free to leave comments below. Don't forget to follow me on Twitter for more updates and insights.