Now that we’ve put together all of the MVVM components, we’re going to take a quick look at the testing across these components.
View Model Tests
We have already looked at the Player
unit test in Lesson 2.1. Now, let’s create some tests for the GameSession
view model. In the SimpleRPG.Game.Engine.Tests project, create the GameSessionTests
class.
using SimpleRPG.Game.Engine.ViewModels;
using Xunit;
namespace SimpleRPG.Game.Engine.Tests.ViewModels
{
public class GameSessionTests
{
[Fact]
public void CreateGameSession()
{
// arrange
// act
var vm = new GameSession();
// assert
Assert.NotNull(vm);
Assert.NotNull(vm.CurrentPlayer);
Assert.Equal("DarthPedro", vm.CurrentPlayer.Name);
Assert.Equal("Fighter", vm.CurrentPlayer.CharacterClass);
Assert.Equal(10, vm.CurrentPlayer.HitPoints);
Assert.Equal(1000, vm.CurrentPlayer.Gold);
Assert.Equal(0, vm.CurrentPlayer.ExperiencePoints);
Assert.Equal(1, vm.CurrentPlayer.Level);
}
[Fact]
public void AddXP()
{
// arrange
var vm = new GameSession();
// act
vm.AddXP();
// assert
Assert.Equal(10, vm.CurrentPlayer.ExperiencePoints);
}
}
}
The CreateGameSession
test simple creates an instance of GameSession
and validates that the properties are what we expect.
The AddXP
test calls GameSession.AddXP
method and verifies the player’s experience was incremented.
These are simple tests, but ensure the behavior of our code is what we expect. Granular, simple tests at the lowest level guard us from breaking our code expectations when we start changing and refactoring code. And these types of test run very quickly… about 2-3ms, so we don’t have to worry about running them frequently.
MainScreen Tests
Let’s create an initial rendering test for our game’s MainScreen.razor page, since our page doesn’t do much more than render the player right now.
In the SimpleRPG.Game.Tests project, create a Pages folder (remember we like to match our code and test folders) and a MainScreenTests
class for our tests. Here’s the code for our test:
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using SimpleRPG.Game.Engine.ViewModels;
using SimpleRPG.Game.Pages;
using SimpleRPG.Game.Tests.Mocks;
using Xunit;
namespace SimpleRPG.Game.Tests.Pages
{
public class MainScreenTests
{
[Fact]
public void SimpleRender()
{
// arrange
using var ctx = new TestContext();
// act
var cut = ctx.RenderComponent<MainScreen>();
// assert
var expected = @"<th scope=""col"" class="""" style="""" blazor:onclick=""2"" rowspan=""2"">";
Assert.Contains(expected, cut.Markup);
Assert.Contains("Player Data", cut.Markup);
Assert.Contains("DarthPedro", cut.Markup);
Assert.Contains("Fighter", cut.Markup);
}
}
}
First, remember that we are using bUnit to test our Blazor components (Lesson 1.8). So, we start the test by creating an instance of the bUnit TestContext
. Then, we render the MainScreen
component (remember pages are components too). Finally, we validate some of the component markup, like we have a Player Data section and the name and character class coming from GameSession
are displayed on the page. Pretty basic test.
If we build and run this test now, we will get the following failure with this test:
System.InvalidOperationException : Cannot provide a value for property 'ComponentMapper' on type 'Blazorise.Row'. There is no registered service of type 'Blazorise.IComponentMapper'.
Recall that we are also using the Blazorise library to build our page (Lesson 1.5). This error is informing us that the required Blazorise services are not initialized correctly in the bUnit TestContext
.
Registering Blazorise Services
Blazorise has a set of services that must be registered and initialized with the TestContext
to enable rendering of their components. Since this is more than one service, we will use a helper method to register them all… rather than calling AddSingleton
for each service in every component/page test.
Let’s create the TestServiceProviderExtensions
class in the Mocks folder, with the following code:
using Blazorise;
using Blazorise.Bootstrap;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace SimpleRPG.Game.Tests.Mocks
{
public static class TestServiceProviderExtensions
{
public static void AddBlazoriseServices(this TestServiceProvider services)
{
services.AddSingleton<IClassProvider>(new BootstrapClassProvider());
services.AddSingleton<IStyleProvider>(new BootstrapStyleProvider());
services.AddSingleton<IJSRunner>(new BootstrapJSRunner(new MockJSRuntime()));
services.AddSingleton<IJSRuntime>(new MockJSRuntime());
services.AddSingleton<IComponentMapper>(new ComponentMapper());
services.AddSingleton<IThemeGenerator>(new BootstrapThemeGenerator());
services.AddSingleton<IIconProvider>(new MockIconProvider());
services.AddSingleton<BlazoriseOptions>(new BlazoriseOptions());
}
class MockJSRuntime : IJSRuntime
{
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
{
return new ValueTask<TValue>();
}
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
{
return new ValueTask<TValue>();
}
}
class MockIconProvider : IIconProvider
{
public bool IconNameAsContent => false;
public string GetIconName(IconName name)
{
return string.Empty;
}
public string GetIconName(string customName)
{
return string.Empty;
}
public string Icon(object name, IconStyle iconStyle)
{
return string.Empty;
}
public void SetIconName(IconName name, string newName)
{
}
}
}
}
The AddBlazoriseServices
is an extension method that adds all of the required Blazorise services to the TestServiceProvider
.
For those not familiar with C# extension methods, they enable us to add methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are static methods, but they’re called as if they were instance methods on the extended type. For calling code, there’s no difference between calling an extension method and the methods defined in a type.
The three important steps to making an extension method are:
- The class must be static.
- The method must be static.
- The method’s first parameter must be the class/interface we are extending with the
this
operator, i.e.:(this TestServiceProvider services)
.
We also added two Mock classes (MockJSRuntime
and MockIconProvider
) that don’t do anything, but allow us to fake their use by Blazorise library.
Fixing Up Page Test
Now that we have the AddBlazoriseServices
method, let’s use it to register the services and get the test passing successfully.
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using SimpleRPG.Game.Engine.ViewModels;
using SimpleRPG.Game.Pages;
using SimpleRPG.Game.Tests.Mocks;
using Xunit;
namespace SimpleRPG.Game.Tests.Pages
{
public class MainScreenTests
{
[Fact]
public void SimpleRender()
{
// arrange
using var ctx = new TestContext();
ctx.Services.AddBlazoriseServices();
// act
var cut = ctx.RenderComponent<MainScreen>();
// assert
var expected = @"<th scope=""col"" class="""" style="""" blazor:onclick=""2"" rowspan=""2"">";
Assert.Contains(expected, cut.Markup);
Assert.Contains("Player Data", cut.Markup);
Assert.Contains("DarthPedro", cut.Markup);
Assert.Contains("Fighter", cut.Markup);
}
}
}
As you can see in line #27, it looks like we are calling a method on TestContext.Services
, but in reality we are calling our helper method… the magic of extension methods.
Now if we build and run the test, we will see that the test runs successfully.
In conclusion, we were able to build tests for all three components of the MVVM design pattern. This lets us target tests at the appropriate layer and cover more of the game logic with unit tests. Unit tests run faster and are typically well isolated, so it makes our testing efforts more effective. Component tests are good for validating the markup (HTML) for the user interface is what we expect.
I am unable to get this line to compile in the TestServiceProvidersExtensions class:
services.AddSingleton(new BlazoriseOptions());
I get a CS7036: There is no argument given that corresponds to the required formal parameter ‘serviceProvider’ of ‘BlazoriseOptions.BlazoriseOptions(IServiceProvider, Action)’
LikeLike
Hi Timothy,
It looks like there has been a breaking change in a recent Blazorise update to that class constructor. I have not updated to the latest Blazorise version. This sample is still on version 0.9.1.2. You may want to downgrade to that version for now to follow along with these lessons, because I am not sure what else might be affected.
It does look like you could change the code to the following to get the test project to compile:
services.AddSingleton(new BlazoriseOptions(services, null));
No guarantee that something else may not trip you up.
I plan to upgrade the sample to .NET 5 and Blazorise 0.9.2.5 soon to get on the latest versions.
Thanks for your feedback.
LikeLike