iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 28
0
Modern Web

今晚,我想來點Blazor系列 第 28

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


上一篇
Day 27:Blazor x Chart.js
下一篇
Day 29:
系列文
今晚,我想來點Blazor30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言