Day 28:元件的单元测试

为什麽要做测试? 今天在外面餐厅吃饭,厨师在出菜前会先试吃看看味道对不对;一台咖啡机,在出厂前也会经过一番测试,确认出水是否有异常? 压力表测压准不准,加热模组有没有正常启动,这些测试都没问题,才会卖到消费者手上。对我们程序设计师来说,我们开发的应用程序就是给使用者的产品,因此在给人使用前,当然也要经过测试。

应用程序在开发初期规模还不大的时候,要验证程序是否如预期,通常都用人工测试的方式来验证。
但随着应用程序越来越庞大,人工测试所耗费的时间越来越多,这个时候可以试着导入自动化测试,自动化测试的第一步就是单元测试,单元测试带来的几个好处:

  • 自动化,执行单元测试的效率比人工快许多
  • 需要重构时,有单元测试保护,就不容易出现改A坏B的情况
  • 从测试案例可以看出应用程序在做什麽和做了什麽
  • 经过测试的应用程序,将给予开发人员足够的信心

说了这麽多单元测试的优点,我们来看看Blazor要怎麽进行单元测试吧
目前微软官方尚未有自己的Blazor测试框架,较知名的是社群开发的bUnit
接下来我们会使用bUnit来测试Blazor专案预设的几个元件

建立好Blazor专案,Server或WebAssembly都可以,再建立一个测试专案。因为bUnit本身需要一个测试框架来执行测试案例,因此建立时可以选一个习惯用的测试框架专案,这边我较熟悉Nunit,所以建立NUnit测试专案(.net Core)
https://ithelp.ithome.com.tw/upload/images/20201012/20130058E0NFmiij9H.jpg

在测试专案安装bUnit。

记得要勾选包括抢鲜版,才会显示bunit。如果刚刚选择的是xUnit测试专案,安装第一个bunit就可以了,如果是MSTest或NUnit,就装bunit.web和bunit.core
https://ithelp.ithome.com.tw/upload/images/20201012/20130058hGFmABXJRj.jpg

安装好後,加入专案参考
https://ithelp.ithome.com.tw/upload/images/20201012/201300584bsRE5wpuY.jpg

接下来可以撰写测试程序了,一开始先来测试最单纯的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>");
        }
  • TestContext物件可以帮我们产生受测元件
  • 受测元件透过MarkupMatches方法,比对元件内的html是否与期待的相同

通过第一个测试罗~
https://ithelp.ithome.com.tw/upload/images/20201012/20130058ObDvyW1LW7.jpg

再来测试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");
        }
  • 一样用RenderComponent取的受测的counter元件,并用Find方法取得p标签
  • Find方法找到button後,呼叫Click事件,然後取得p标签的文字内容
  • 比对文字内容是否为Current count: 1

Counter也通过测试罗
https://ithelp.ithome.com.tw/upload/images/20201012/20130058pSuLXFwZmR.jpg

测试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);
    }
}
  • OnInitializedAsync改用weatherService的GetWeatherDataAsync方法

确认FetchData可以正常执行
https://ithelp.ithome.com.tw/upload/images/20201012/20130058vev18E1Zf7.jpg

FetchData有2个情境要测式:

  1. 没有资料时,显示loading...
  2. 有资料时,用table显示

这边使用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);
        }

测试成功
https://ithelp.ithome.com.tw/upload/images/20201012/201300587f6SG2z0qm.jpg

接着测试第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://ithelp.ithome.com.tw/upload/images/20201012/20130058qrLg5hvzAj.jpg

程序码可参考:https://github.com/CircleLin/BlazorUITest


<<:  不用Recoil的话,如何自己制作一个 Custom hook 来共享全域变数?

>>:  Day27 - HTML 与 CSS (9) - head

部署model on seldon(MinIO)

上一篇我们已使用notebook已经将训练好的model上传到MinIO储存空间, 本篇我们将使用s...

我想当工程师!要念资讯相关科系吗?

在业界蛮多如何成为工程师的课程,至於要不要念本科系,以现今的社会来说不一定是必要条件。相关科系从事相...

Day06:【TypeScript 学起来】资料型别那些事 : 总览

Q: 为什麽工程师都喜欢用 dark mode? A: 因为太亮会吸引很多 bug。 原来如此XD...

30天学会 Python: Day 1-印啦!哪次不印!

型别 型别指的是资料的型态,Python 内建的几个基本型态有: 数字 整数(Integer)-in...

[鼠年全马] W39 - 使用Vuex管理资料状态(下)

这周要继续来探讨 Vuex 上周的文章传送门 首先先回顾一下上周提到的 Store 中有这些东西: ...