为什麽要做测试? 今天在外面餐厅吃饭,厨师在出菜前会先试吃看看味道对不对;一台咖啡机,在出厂前也会经过一番测试,确认出水是否有异常? 压力表测压准不准,加热模组有没有正常启动,这些测试都没问题,才会卖到消费者手上。对我们程序设计师来说,我们开发的应用程序就是给使用者的产品,因此在给人使用前,当然也要经过测试。
应用程序在开发初期规模还不大的时候,要验证程序是否如预期,通常都用人工测试的方式来验证。
但随着应用程序越来越庞大,人工测试所耗费的时间越来越多,这个时候可以试着导入自动化测试,自动化测试的第一步就是单元测试,单元测试带来的几个好处:
说了这麽多单元测试的优点,我们来看看Blazor要怎麽进行单元测试吧
目前微软官方尚未有自己的Blazor测试框架,较知名的是社群开发的bUnit。
接下来我们会使用bUnit来测试Blazor专案预设的几个元件
建立好Blazor专案,Server或WebAssembly都可以,再建立一个测试专案。因为bUnit本身需要一个测试框架来执行测试案例,因此建立时可以选一个习惯用的测试框架专案,这边我较熟悉Nunit,所以建立NUnit测试专案(.net Core)
在测试专案安装bUnit。
记得要勾选包括抢鲜版,才会显示bunit。如果刚刚选择的是xUnit测试专案,安装第一个bunit就可以了,如果是MSTest或NUnit,就装bunit.web和bunit.core
安装好後,加入专案参考
接下来可以撰写测试程序了,一开始先来测试最单纯的index元件
[Test]
public void IndexShouldRender()
{
var ctx = new Bunit.TestContext();
//cut = component under test
var cut = ctx.RenderComponent<BlazorUITest.Pages.Index>();
cut.MarkupMatches("<h1>Hello, world!</h1>");
}
通过第一个测试罗~
再来测试Counter元件,Counter元件中每按一下button,p标签内的数字会加1,因此我们准备来测试这个行为
[Test]
public void CounterShouldIncrementWhenSelected()
{
var ctx = new Bunit.TestContext();
//Arrange
var cut = ctx.RenderComponent<Counter>();
var element = cut.Find("p");
//Act
cut.Find("button").Click();
string elementText = element.TextContent;
//Assert
elementText.MarkupMatches("Current count: 1");
}
Counter也通过测试罗
测试FetchData元件
在FetchData中,初始化时会透过HttpClient取得json资料,但因为HttpClient不是介面所以不容易mock,因此我们另外建立一个WeatherService和介面IWeatherService,将OnInitializedAsync中的Http.GetFromJsonAsync搬到WeatherService内:
public class WeatherService : IWeatherService
{
public async Task<WeatherForecast[]> GetWeatherDataAsync()
{
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:56692/");
WeatherForecast[] data = await httpClient.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
return data;
}
}
在programs.cs注册IWeatherService:
builder.Services.AddScoped<IWeatherService, WeatherService>();
原本FetchData注入HttpClient,改成注入IWeatherService:
@page "/fetchdata"
@inject IWeatherService weatherService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
//略...
@code {
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await weatherService.GetWeatherDataAsync();
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
确认FetchData可以正常执行
FetchData有2个情境要测式:
这边使用NSubStitute mocking library,来mock刚刚建立的IWeatherService
第一个情境,没有资料时,显示loading
[Test]
public void FetchDataShouldRenderLoadingWhenDataIsNull()
{
var ctx = new Bunit.TestContext();
//mock WeatherService
var mockService = Substitute.For<IWeatherService>();
//设定WeatherService的GetWeatherDataAsync方法回传null
mockService.GetWeatherDataAsync().Returns(Task.FromResult<FetchData.WeatherForecast[]>(null));
//注册到Services
ctx.Services.AddSingleton<IWeatherService>(mockService);
var cut = ctx.RenderComponent<FetchData>();
var expectedHtml = @"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<p><em>Loading...</em></p>";
cut.MarkupMatches(expectedHtml);
}
测试成功
接着测试第2个情境,有资料时用table显示
[Test]
public void FetchDataShouldRenderLoadingWhenDataIsNotNull()
{
var ctx = new Bunit.TestContext();
var mockService = Substitute.For<IWeatherService>();
mockService.GetWeatherDataAsync().Returns(Task.FromResult(new WeatherForecast[] { new WeatherForecast() { TemperatureC = 30, Summary = "test", Date = new DateTime(2020, 10, 6) } }));
ctx.Services.AddSingleton<IWeatherService>(mockService);
var cut = ctx.RenderComponent<FetchData>();
var expectedHtml = @"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<table class='table'>
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>2020/10/6</td>
<td>30</td>
<td>85</td>
<td>test</td>
</tr>
</tbody>
</table>";
cut.MarkupMatches(expectedHtml);
}
测试成功
程序码可参考:https://github.com/CircleLin/BlazorUITest
<<: 不用Recoil的话,如何自己制作一个 Custom hook 来共享全域变数?
>>: Day27 - HTML 与 CSS (9) - head
上一篇我们已使用notebook已经将训练好的model上传到MinIO储存空间, 本篇我们将使用s...
在业界蛮多如何成为工程师的课程,至於要不要念本科系,以现今的社会来说不一定是必要条件。相关科系从事相...
Q: 为什麽工程师都喜欢用 dark mode? A: 因为太亮会吸引很多 bug。 原来如此XD...
型别 型别指的是资料的型态,Python 内建的几个基本型态有: 数字 整数(Integer)-in...
这周要继续来探讨 Vuex 上周的文章传送门 首先先回顾一下上周提到的 Store 中有这些东西: ...