在Blazor中测试。一个完整的教程

1,094 阅读7分钟

简介

在C#开发中引入Blazor,使开发人员有能力将其开发扩展到浏览器中,而无需依赖React、Vue.js和Angular等传统JavaScript框架。

虽然在传统的JavaScript框架中设置测试比较容易,但Blazor需要将一些工具和包整合在一起,然后了解如何以及在应用程序中测试什么。

这篇文章将介绍如何为一个简单的Blazor计数器应用程序设置测试,并将其扩展到包括C#开发人员可能想在Blazor应用程序中测试的几乎所有内容。

建立一个测试环境

首先,我们来设置演示项目。

创建一个新项目

在Visual Studio中,点击 "新建"。

Screenshot of Visual Studio homepage with arrow pointing to the "+new" button

Web和控制台菜单中,选择应用程序,然后选择Blazor服务器应用程序

Visual Studio new project page

在下一页中,继续不进行验证,然后设置项目名称解决方案名称。单击 "创建"。

设置一个测试项目

要设置一个测试项目,从文件菜单的下拉菜单中点击新建解决方案;应该会弹出一个模板窗口。

从左侧边栏的Web 控制台组,选择测试,选择xUnit测试项目,然后点击下一步

Visual Studio new test project page

使用与主项目相同的框架版本,然后点击下一步

Configure new xUnit test project page

最后,为解决方案和项目设置一个名称,然后点击创建

一旦完成,你的Visual Studio应该有如下的侧边栏。

Visual Studio with Blazor sample app set up

将主项目链接到测试项目

为了使测试项目能够引用和使用主项目,我们必须在测试项目中创建一个链接,这样我们就可以从主项目中导入和使用组件、类和接口。

在Visual Studio里面,从左边的侧边栏中右击测试解决方案,选择编辑项目文件,然后在同一组里面添加<ProjectReference Include="../path/to/main-project/main-project.csproj" /> ,并添加SDK版本。

Gif of inputting code to link the main and test Blazor apps

设置测试依赖性

安装bUnit

项目菜单中,点击管理NuGet包,搜索bUnit,选择bUnitbUnit.core,点击添加包,选择两个解决方案,然后点击OK

Gif of bUnit being added to project

安装xUnit

这个测试项目被引导为xUnit项目。默认情况下,它自带xUnit包。

安装Moq

Moq是一个断言库,对于测试预期结果是否与返回的结果相匹配很有用。

我们可以用安装bUnit的同样方法来安装Moq。只需搜索并选择Moq,点击添加包,选择测试项目,然后点击OK

用bUnit测试

xUnit是一个测试框架,它提供了一个接口,可以在浏览器之外运行Blazor应用程序,并仍然通过代码与输出进行交互。

bUnit是一个接口,我们可以通过它与Blazor组件互动。bUnit提供的接口使我们有可能在Blazor组件上触发事件,找到组件上的一些元素,并作出断言。

测试设置

要用bUnit测试Blazor应用程序,测试套件必须在测试项目中的一个类中有一个测试用例功能。

测试用例中的代码应该有以下内容。

  • Arrange, 设置一个TestContext (一个用于渲染Blazor组件的虚拟环境)。
  • Act ,将一个组件渲染到测试环境中,触发动作,并提出网络请求。
  • Assert ,检查事件是否被触发以及是否显示了正确的文本。

作为一个例子,下面的设置说明了上述步骤。

using BlazorApp.Pages;
using Bunit;
using Xunit;

namespace BlazorAppTests
{
    public class CounterTest
    {
        [Fact]
        public void RendersSuccessfully()
        {

            using var ctx = new TestContext();

            // Render Counter component.
            var component = ctx.RenderComponent<Counter>();

            // Assert: first, find the parent_name vital element, then verify its content.
            Assert.Equal("Click me", component.Find($".btn").TextContent);
        }

    }
}

从右边的侧边栏,点击测试,然后点击全部运行来运行这个测试。

Gif of running a test in Blazor

传递参数给组件

有时,组件需要参数才能正确呈现。bUnit 提供了一个接口来处理这个问题。

首先,让我们修改应用程序解决方案中的counter 组件,使其看起来像下面这样。

@page "/counter/{DefaultCount:int?}"

<h1>Counter</h1>

<p>Current count: <span id="counterVal">@currentCount</span></p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;


    [Parameter]
    public int DefaultCount { get; set; }

    protected override void OnParametersSet()
    {
        if (DefaultCount != 0)
        {
            currentCount = DefaultCount;
        }
    }

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

首先,注意到我们如何更新了路径,以接受一个DefaultCount 的参数,即一个整数。? 告诉Blazor,这个参数是可选的,对组件的运行不是必需的。

接下来,注意到C#代码中的DefaultCount 属性有一个[Parameter] 属性。我们已经将OnParametersSet 生命周期方法挂起,以便在参数被设置时通知组件。这确保我们用它来更新组件currentValue 属性,而不是让组件从零开始计数。

我们可以在bUnit测试用例中用以下方法渲染这个组件。

using BlazorApp.Pages;
using Bunit;
using Xunit;

namespace BlazorAppTests
{
    public class CounterTest
    {
        public void RendersSuccessfully()
        {

            using var ctx = new TestContext();

            Action onBtnClickHandler = () => { };

            // Render Counter component.
            var component = ctx.RenderComponent<Counter>(
              parameters =>
                parameters
                      // Add parameters
                  .Add(c => c.DefaultCount, 10)
                  .Add(c => c.OnBtnClick, onBtnClickHandler)
            );


            // Assert: first find the parent_name strong element, then verify its content.
            Assert.Equal("Click me", component.Find($".btn").TextContent);
        }

    }
}

在上面的测试中的第14行,我们渲染组件,然后传递一个回调给组件,调用(p => );

然后,我们将Add 方法添加到参数(p => p.Add(c => c.DefaultCount, 10); ,以便将该参数设置为10。

我们可以用同样的方法传递一个事件回调,即p.Add(c => c.onBtnClickHandler, onBtnClickHandler) 。这样,我们在onBtnClickHandler 动作中实现了计数器的递增,而不是在counter 组件中。

将输入和服务传递给组件

有些组件依靠外部服务来运行,而有些则依靠外部字段。我们可以通过测试上下文中的Services.AddSingleton 方法,用bUnit来实现这一点。

在演示的计数器应用里面,有一个FetchData.razor 文件,它严重依赖一个WeatherForecastService 服务。让我们尝试在xUnit测试项目中运行这个文件。

在测试项目中创建一个名为FetchDataTest.cs 的新文件,并添加以下内容。

using System;
using BlazorApp.Data;
using BlazorApp.Pages;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace BlazorAppTests
{
    public class FetchDataTest
    {
        [Fact]
        public void RendersSuccessfully()
        {

            using var ctx = new TestContext();

            ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());

            // Render Counter component.
            var component = ctx.RenderComponent<FetchData>();

            Assert.Equal("Weather forecast", component.Find($"h1").TextContent);
        }

    }
}

注意我们是如何使用AddSingleton 接口来添加一个新的服务到我们的测试运行器上下文的。而当我们运行这个测试文件时,我们应该得到一个成功的结果。

事件

上面,我们看到了如何在测试用例组件内为一个事件设置回调。让我们看看如何在组件内的一个元素上触发事件。

计数器测试文件有一个按钮,当点击时,会增加计数器。让我们测试一下,确保我们可以点击这个按钮,看到页面上的计数更新。

在测试项目中的CounterTest.cs 文件内,在CounterTest 测试套件类中添加以下测试案例。

[Fact]
public void ButtonClickAndUpdatesCount()
{
    // Arrange
    using var ctx = new TestContext();
    var component = ctx.RenderComponent<Counter>();

    // Render
    var counterValue = "0";
    Assert.Equal(counterValue, component.Find($"#counterVal").TextContent);

    counterValue = "1";
    var buttonElement = component.Find("button");

    buttonElement.Click();

    Assert.Equal(counterValue, component.Find($"#counterVal").TextContent);
}

在 "排列 "部分设置了该组件。像往常一样,在 "Render "部分,我们首先断言该组件从零开始。

然后,我们使用测试上下文组件的.Find 接口获得按钮的引用,这时返回元素的引用,它也有一些像Click() 方法的API。

最后,我们断言组件的值,以确认按钮的点击会做同样的动作。

等待异步的状态更新

请注意,在注入服务后,我们没有测试是否有任何数据被渲染。就像FetchData.razor 组件一样,有些组件需要时间才能渲染出正确的数据。

我们可以通过component.waitForState(fn, duration) 方法来等待异步状态的更新。

[Fact]
public void RendersServiceDataSuccessfully()
{

    using var ctx = new TestContext();

    ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());

    // Render Counter component.
    var component = ctx.RenderComponent<FetchData>();

    component.WaitForState(() => component.Find(".date").TextContent == "Date");


    Assert.Equal("TABLE", component.Find($".table").NodeName);
}

上面的例子等待异步数据的加载,直到WaitForState 中的匿名函数被调用,该函数测试找到一个具有date 类的元素。一旦找到了,我们就可以对结果做一些进一步的断言。

验证标记

我们还可以通过MarkupMatches bUnit接口方法验证一个组件的标记是否遵循相同的模式。

例如,我们可以测试索引是否包含有 "Hello, World!"文本内容的h1

首先,在测试项目内创建一个新文件,命名为IndexTest.cs ,并添加以下内容。

using System;
using BlazorApp.Pages;
using Bunit;
using Xunit;

namespace BlazorAppTests
{
    public class IndexTest
    {
        [Fact]
        public void RendersSuccessfully()
        {

            using var ctx = new TestContext();

            // Act
            var component = ctx.RenderComponent<BlazorApp.Pages.Index>();

            // Assert
            Assert.Equal("Hello, world!", component.Find($"h1").TextContent);
        }

    }
}

除此以外,我们还可以通过.Find (我们已经在这样做了),和FindAll ,来验证一个组件是否包含一个元素,它可以返回所有与查询相匹配的特征。这些方法采用了类似于CSS的选择器,这使我们更容易遍历节点。

嘲弄IJSRuntime

IJSRuntime是一个接口,它使得从.Net代码中与JavaScript交互成为可能。

一些组件可能依赖于它;例如,一个组件可以使用jQuery方法来进行API调用。

如果我们的项目中有JavaScript函数getPageTitle ,我们可以模拟该函数的调用,这样在我们组件的任何地方,其结果将是我们在测试案例中可能指定的。

using var ctx = new TestContext();

ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());

var theResult = "some result";
ctx.JSInterop.Setup<string>("getPageTitme").SetResult(theResult);

// Render Counter component.
var component = ctx.RenderComponent<FetchData>();


Assert.Equal(theResult, component.Find($".page-title").TextContent);

嘲弄HttpClient

一些应用程序依靠来自远程服务器的数据来正常运行。

单元测试的部分策略是使每个测试用例的依赖性不受影响。而依靠HTTP客户端接触到远程服务器的组件来呈现一个功能,如果结果不是静态的,就会破坏我们的测试。

我们可以通过模拟HTTPClient来消除这个问题,HTTPClient是一个可以从Blazor应用内部向外部世界发出HTTP请求的库。

根据bUnit的文档,bUnit默认不包含这个功能,但我们可以依靠第三方库来实现这个功能。

首先,将RichardSzalay.MockHttp包添加到测试项目中。

dotnet add package RichardSzalay.MockHttp --version 6.0.0

接下来,在测试项目的根部创建一个名为MockHttpClientBunitHelpers 的文件,并添加以下内容。

using Bunit;
using Microsoft.Extensions.DependencyInjection;
using RichardSzalay.MockHttp;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;

public static class MockHttpClientBunitHelpers
{
    public static MockHttpMessageHandler AddMockHttpClient(this TestServiceProvider services)
    {
        var mockHttpHandler = new MockHttpMessageHandler();
        var httpClient = mockHttpHandler.ToHttpClient();
        httpClient.BaseAddress = new Uri("http://localhost");
        services.AddSingleton<HttpClient>(httpClient);
        return mockHttpHandler;
    }

    public static MockedRequest RespondJson<T>(this MockedRequest request, T content)
    {
        request.Respond(req =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK);
            response.Content = new StringContent(JsonSerializer.Serialize(content));
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            return response;
        });
        return request;
    }

    public static MockedRequest RespondJson<T>(this MockedRequest request, Func<T> contentProvider)
    {
        request.Respond(req =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK);
            response.Content = new StringContent(JsonSerializer.Serialize(contentProvider()));
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            return response;
        });
        return request;
    }
}

现在,创建一个新的测试案例,并添加以下内容。

[Fact]
public void FetchResultTest()
{
    var serverTime = "1632114204";
    using var ctx = new TestContext();
    var mock = ctx.Services.AddMockHttpClient();
    mock.When("/getTime").RespondJson<string>(serverTime);

    // Render Counter component.
    var component = ctx.RenderComponent<FetchData>();

    Assert.Equal(serverTime, component.Find($".time").TextContent);

}

在这里,我们声明了一个变量,用来保存我们对服务器的期望,然后通过一个bUnit辅助方法ctx.Services.AddMockHttpClient ,将模拟的客户端添加到上下文服务中,该方法将寻找MockHttpClientBunitHelpers ,并将其注入到上下文。

然后,我们使用模拟的引用来模拟我们期望从路由中得到的响应。最后,我们断言我们组件的一部分具有我们从模拟请求返回的值。

总结

在这篇文章中,我们看到了如何设置一个Blazor项目并添加另一个xUnit测试项目。我们还将bUnit作为一个测试框架,并讨论了使用bUnit来测试Blazor组件。

除了xUnit作为一个测试框架外,bUnit还可以在nUnit测试框架中使用类似的概念和API运行。

在这篇文章中,我们介绍了bUnit的一般用法。高级用法可在bUnit文档网站上找到。

The postTesting in Blazor:完整的教程》首先出现在LogRocket博客上。