preloader
image

單元測試

寫這篇緣故

最近在幫公司講解關於單元測試的技術,擷取部分在91課程上寫測試要點還有自己寫測試的經驗,做一個整理,當作是幫公司培訓工程師之類
不過我建議還是給外面的老師指點和上一下會比較好,因為會比較清晰!

單元測試常見的名詞

  • SUT:System Under Test/Software Under Test 【待測程式】,在Unit就是待測試的項目,EX:我們要測的是類別、物件還是方法,都可以統稱 SUT。
  • DOC:Depended-on Component【相依元件】,例如:成立訂單函數,如果訂單失敗異常會寫入log,log函數就是成立訂單函數的DOC。

    在實務上筆者在寫單元測試的時候,因為SUT會去呼叫其他的物件也會呈現出SUT依賴DOC, 在測試SUT勢必會有DOC存在,搞得測試很複雜。

單元測試的3A

  • Arrange – 初始化-白話一點就是準備要測的資料,更詳細一點說穿就是在準備演員和劇本
  • Act – 執行測試的目標,並取得實際結果
  • Assert – 驗證結果
   [TestClass]
    public class CalculatorTest
    {
        [TestMethod]
        public void Calculator_TenAddTwenty_ReturnThirty()
        {
            //Arrange
            var calc = new Calculator();
            int x = 10, y = 20;
            //Act
            int actual = calc.Add(x, y);
            //Assert
            int expected = 30;
            Assert.AreEqual(expected, actual);
        }
    }

    public class Calculator
    {
        public int Add(int x, int y)
        {
            return x + y;
        }
    }

為何要寫單元測試

不知你可曾遇過這樣的狀況….

情境一: 當你接手時候一個舊系統,經過團隊交接輪替狀況下,業主或是客戶要你改一個功能,你卻不知道從哪下手呢?

單元測試能幫你做什麼?
單元測試他就像活者的情境測試的規格書,就像是API的input和output,我要預期得到什麼樣結果來進行做驗證,他就像是一個情境的案例。

情境二: 是否曾經改發生過改好新功能,但舊功能就壞掉,修好舊功能,新功能又壞掉了呢?

單元測試能幫你做什麼?
它能告訴你目前的程式,是否執行都符合當時所要求的規格與產出結果,如果舊規格舊方法沒有改的情況下,單元測試壞了,那就表示你這次的改動絕對有影響到它,趕緊去修好它吧!!

情境三 : 你在計算關鍵的金額的時候,偵錯下去才發現,天殺的,原來是自己觀念上錯誤或不熟悉。例如Double a = 1.1加上Double b = 1.2,而你以為它的結果會是2.3….

單元測試能幫你做什麼?
它能即時的驗證你的想法,而不是到上線時才賭人品,尤其是那些你覺得理所當然會對的功能中,魔鬼往往藏在細節裡

在你還沒寫單元測試之前,上述狀況是否都似曾相識呢?如果你想解決這些問題,那麼開始寫單元測試將是可以大幅度降低這些錯誤的有效方法。

lab-1:電商活動爸爸節促銷

Unit Test 神兵利器 — 擷取與覆寫 (Extract and override) 在原類別

  • 找出相依物件。
  • 開始進行提取方法(Extract Method)。
  • 將該private改成protected的方法。

在測試類別

  • 繼承原類別,命名為FakeXXXX。
  • 覆寫FakeXXXXX中的protected方法。
  • 針對protected方法中的物件,建立其setter
  • 在測試中,新建FakeXXXX的實例,然後根據情境修改不同的值。
  • 測試。

Unit Test 神兵利器 — 相依注入(Dependency Injection)

  1. 針對外部相依物件,建立其介面(Interface)
  2. 將新增的介面,加上相依的方法
  3. 將原本使用相依物件地方,更改為使用其介面。

什麼是屬於不好的單元測試


    public class UserService
    {
        public UserResponseViewModel Login(string id, string pwd)
        {
            var userResponseViewModel = new UserResponseViewModel();
            UserRepository userRepository = new UserRepository();
            RoleRepository roleRepository = new RoleRepository();
            var user = userRepository.GetUser(id, pwd);
            if (user != null)
            {
                var roleName = roleRepository.GetRoleName(user.RoleId);
                if (!string.IsNullOrEmpty(roleName))
                {
                    userResponseViewModel.IsSuccess = true;
                    userResponseViewModel.Name = user.Name;
                    userResponseViewModel.RoleName = roleName;
                    return userResponseViewModel;
                }
            }
            return userResponseViewModel;
        }
    }

測試程式如下:

        [Test]
        public void LoginSuccess()
        {
            var expected = new userResponseViewModel
            {
                IsSuccess = true,
                Name = "老大",
                RoleName = "ADMIN"
            };
            var userService = new UserService();
            var result = userService.Login("ADMIN", "ADMIN");
            expected.Should().BeEquivalentTo(result);
        }

上述這樣狀況會有

  • 測試DB改資料怎麼辦?
  • DB TimeOut怎麼辦?
  • DB沒有資料阿…
  • 網路沒開…
  • 跑得這麼慢

這測試有哪些狀況

  • 不穩
  • 不方便

沒人想跑,會認為寫單元測試不好,沒效果,浪費時間。

外部物件:DB、API、時間…等

所以問題出在不可控,所以我們要讓它變成可控,那就是要變成假物件。

lab-2:系統管理模組-會員登入

筆者的產品迭代方式

不可測試的爛code→可測試的爛code→可測試的好code

大部分都是遇到爛Code頂多是進化到寫可測試的濫Code不要壞掉,然後再進一步優化改成好的測試Code跟情境有關測試。

筆者常用的測試替身技巧

  • Stub
  • Mock

以Stub例子來說:我們要測試登入用的程式是不是有正常運作。塞了一個假的httpClinet物件給他,只要對httpClinet.Post總是回傳 200 OK。這個 httpClinet物件就是Stub。

Mock:Mock注重的是對於行為的測試。 例如:你要測試寫log行為,就可以測試SUT中的Log來呼叫WriteLog()。

單元測試逃脫不了以下該具備的技能

  1. 物件導向、EX:SOLID、OOP
  2. 重構

如何在組織或既有工作上進行單元測試的第一步?

  • 先從bug和小需求慢慢追加,沒有改到地方,不要隨意重構和調整
  • 新的專案開始進行加入測試專案
  • 從主分支在切一個分支出來,進行交流和試試水溫

筆者會用到的測試框架

  • Nunit 3、NUnit3TestAdapter-單元測試框架
  • NSubstitute-測試替身框架
  • FluentAssertions-專門用來集合比對集合用、讓測試程式更能口語化


自動測試 VS 手動測試

項目 自動測試 手動測試
開發時程
測試時程 極快 (秒/分) 系統越大越慢 (分/時)
測試完整度 依照覆蓋率判斷 落觀音
系統更新掌握度 有把握自我意識良好, 沒把握時自己嚇自己
持久性 無限 人類會心累
CI/CD 可自動化 人工測試/人工佈署
改A壞B 及時反應 不明不白



假如現在有一陀爛CODE,要增加新功能在那堆爛CODE裡,先把爛CODE抽成method,然後再抽成新的Class針對爛CODE的public的情境先寫測試,會讓需要寫測試的範圍變小很多。

那什麼東西的投資報酬率最高?

  1. 要修正的BUG(BUG越晚發現修正成本就更高)
  2. 實務上常跑到的scenario
  3. 最主要的情境
  4. 和錢有關的
  5. 和人命有關的(EX:自動駕駛系統)
  6. 最常改到的CODE,可以避免被改錯

測試的品質

  • 測試的程式一定要重構
  • 測試的語意一定要清楚明白,這是為了讓測試更容易理解,可以很簡單的的從測試的程式碼由語意就能理解這個測試要做什麼
  • 寫測試的難度會反應程式的好壞

最後幾個常見的問題

1.private要不要寫測試?
A:筆者不會寫這一部分測試,原因是private的上層public呼叫的時候,其實public上層有寫測試的話,private就沒必要測了。

2.關於Static method要不要寫測試?
A:通常這部分Static的函數,我就會看狀況採用DI方式,進行寫測試,筆者很少再寫Static Method。

3.入門單元測測試的話,如何進入這個部分?
A:了解如何【隔離相依】、Stub與Mock的差別,如何寫優秀的單元測試。

補充:測試不應該包含if else for while這些程式邏輯…

引用資料:

測試中常見的名詞:Stub, Dummy, Mock..等等
一起設計出可被單元測試的程式碼
91-課程部分內容-針對遺留代碼加入單元測試的藝術

書籍方向

  • 單元測試的藝術第二版

總結:測試就是買保險

comments powered by Disqus
comments powered by Disqus