在Blazor中测试。一个完整的教程
简介
在C#开发中引入Blazor,使开发人员有能力将其开发扩展到浏览器中,而无需依赖React、Vue.js和Angular等传统JavaScript框架。
虽然在传统的JavaScript框架中设置测试比较容易,但Blazor需要将一些工具和包整合在一起,然后了解如何以及在应用程序中测试什么。
这篇文章将介绍如何为一个简单的Blazor计数器应用程序设置测试,并将其扩展到包括C#开发人员可能想在Blazor应用程序中测试的几乎所有内容。
建立一个测试环境
首先,我们来设置演示项目。
创建一个新项目
在Visual Studio中,点击 "新建"。
从Web和控制台菜单中,选择应用程序,然后选择Blazor服务器应用程序。
在下一页中,继续不进行验证,然后设置项目名称和解决方案名称。单击 "创建"。
设置一个测试项目
要设置一个测试项目,从文件菜单的下拉菜单中点击新建解决方案;应该会弹出一个模板窗口。
从左侧边栏的Web 和 控制台组,选择测试,选择xUnit测试项目,然后点击下一步。
使用与主项目相同的框架版本,然后点击下一步。
最后,为解决方案和项目设置一个名称,然后点击创建。
一旦完成,你的Visual Studio应该有如下的侧边栏。
将主项目链接到测试项目
为了使测试项目能够引用和使用主项目,我们必须在测试项目中创建一个链接,这样我们就可以从主项目中导入和使用组件、类和接口。
在Visual Studio里面,从左边的侧边栏中右击测试解决方案,选择编辑项目文件,然后在同一组里面添加<ProjectReference Include="../path/to/main-project/main-project.csproj" />
,并添加SDK版本。
设置测试依赖性
安装bUnit
从项目菜单中,点击管理NuGet包,搜索bUnit,选择bUnit和bUnit.core,点击添加包,选择两个解决方案,然后点击OK。
安装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);
}
}
}
```
从右边的侧边栏,点击测试,然后点击全部运行来运行这个测试。
传递参数给组件
有时,组件需要参数才能正确呈现。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
var theResult = "some result";
ctx.JSInterop.Setup
// 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
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
// 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博客上。
- 在C 中把字符串转换为整数的两种简单方法
- 如何在Flutter中实现任何UI
- Gatsby v4的新内容
- 创建一个Puppeteer微服务以部署到Google Cloud Functions
- 在Blazor中测试。一个完整的教程
- 在React中使用Plotly来构建动态图表
- 分页、加载更多按钮和无限滚动的指南
- 用新的Firebase v9.x Web SDK重构一个React应用
- 在使用地理定位API时,你需要知道什么?
- 在PostgreSQL v14中,JSON有什么新功能?
- 使用React的函数式编程的基础知识
- 使用Dart FFI访问Flutter中的本地库
- 使用视频播放器插件在Flutter中处理视频
- 改进过度约束的Rust库API
- 用Svelte建立一个PWA
- 用Flask和D3.js构建交互式图表
- 在Go中使用JSON。带例子的指南
- 一篇文章入门Unix中的AWK命令!
- C 哈希
- Dotfiles - 什么是Dotfile以及如何在Mac和Linux中创建它