Lesson 2.4: Testing the MVVM Components

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:

  1. The class must be static.
  2. The method must be static.
  3. 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.

2 thoughts on “Lesson 2.4: Testing the MVVM Components

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s