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

语言: CN / TW / HK

简介

在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?}"

Counter

Current count: @currentCount

@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();

// 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(new WeatherForecastService());

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

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

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); 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(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博客上。