我與Unit Test的起源

凡事都有第一次,第一次聽到單元測試是在上一間公司的學長建議我去接觸看看,一開始接觸也是懵懵懂懂上網Google一些資料還有TDD開發的相關資訊

大概了解到它是滿多開發人員推薦的一種開發模式,直到最近到了新公司以後,公司開始要在專案中引入單元測試對它才有更進一步的了解。

為什麼要單元測試?

一開始接觸單元測試的時候不太清楚為什麼需要撰寫測試,程式運行後正常執行就好了,如果有報錯的話就回去修改即可

另外是寫測試程式等於在開發的時候要再額外寫一個程式,在開發的進度上也會花費更多的時間,尤其對於沒寫過單元測試經驗的工程師來說寫測試的時間可能比寫主要開發程序的時間來的長。

所以我們就不用寫單元測試嗎?

我們都知道單元測試是一個好東西,對於專案會有很大的幫助,但是總要有起步的那一天,如果不去實踐它永遠沒有開始的那一天

如果我們開始寫單元測試會不會拖慢我們的開發進度?

這的確是一個好問題,最近在投資與金濟學中意外的得到了這個問題的解答

在投資理財中投資人大多都會因為投資所相伴的風險卻步,但是我們有沒有想過不投資的風險?

我們的儲蓄終究沒辦法抵抗通貨膨脹的侵蝕,對於單元測試也是如此,對於一開始要撰寫單元測試的開發人員來說,我們只想到了開始寫單元測試必須面對的風險是可能拖慢開發的進度,似乎不曾想過不寫單元測式的風險。

再舉一個生活貼切的例子,某個禮拜五的下午接到PM的訊息,告知你客戶剛剛發現專案似乎發生了點問題

希望你能在周末假期之前修好,這樣就不需要打擾到你愉快的周末。

聰明睿智的你很快的就發現問題點在哪邊

心想喔!這就只是個小問題,稍微修改一下程式碼就可以快樂的下班過一個愉快的周末了,理想是那麼的美好

但當你改了這一個小部分,突然一切都變得不正常了,其他的部分開始出錯,於是我們開始花更多的時間來尋找錯誤,等到下班的時間到了,PM過來詢問你狀況,你這時候可能只能回答:我需要…..更多的咖啡

恩……以上的情節絕對不是我們想碰到的,那可能會犧牲掉我們一整個周末

如果有撰寫好單元測試,就可以使得我們得專案更加堅固

讓我們再修改程式碼的時候知道更改了這行會不會造成其他地方出現Bug,另外你有聽說過嗎?

你就是不寫測試才會沒時間

在專案中引入單元測試比較像是一種倒吃甘蔗的行為,萬事起頭難、但只要建立起來以後對開發人員是一種保障,可以更加得有自信去修改重構你的code

使得CI/CD更加得順利安定,沒有自動化測試採取敏捷開發的專案,面對大量的需求變更將會非常難以維護

沒有測試的大規模重構都是一種危險的投機行為

.Net 單元測試

以上廢話完了 進入程式碼環節

這次利用一個簡單的小例子來實作單元測試

情境是這樣的我們有一輛AE86,AE86依賴油門來驅動

油門供給多的時候車子跑得比較快,油門供給少的話車子跑得比較慢甚至不動,油門狂催的結果會是 Deja vu


有一個油箱供應的class 固定是供給給油門100的油

public class GasSupply
{
    public int GetGas()
    {
        return 100;
    }
}

再創造一個AE86的Class

當油門小於等於0的時候是沒辦法發動

小於100的速度是Slow

小於200是速度是Min

大於200則是Deja vu!

class AE86
{
    private GasSupply _gasSupply;
    public AE86(GasSupply gasSupply)
    {
        _gasSupply = gasSupply;
    }

    public string Run()
    {
        int accelerator = _gasSupply.GetGas();

        if (accelerator <= 0)
        {
            return "Won't start";
        }
        else if (accelerator < 100)
        {
            return "Slow";
        }
        else if (accelerator < 200)
        {
            return "Min";
        }
        else
        {
            return "Deja vu!";
        }
    }
}
var car = new AE86(new GasSupply());

Console.WriteLine(car.Run());

由於我們目前的GasSupply是寫死的100 目前車子發動的時候速度都會是Min

如果現在我們把供油改成210 會變成Deja vu!

依造目前的情況來說看起來還不錯,但是大家有沒有發現一個問題,如果想要測試我們的AE86的速度的話我們必須去直接修改GasSupply,這在程式的架構中是不對的,當一個class設計好之後是不太能夠任意的去修改它的代碼的,假如今天我們的GasSupply不止給AE86提供油料而是還有其他的車子,其他的車子油門轉換的速度是跟AE86不同的這樣就會有問題了。

那怎麼辦呢?這時候我們就必須引入interface來進行解耦合,再利用單元測試來測試我們的AE86

public interface IGasSupply
{
    int GetGas();
}
public class GasSupply : IGasSupply
{
    public int GetGas()
    {
        return 100;
    }
}
public class AE86
{
    private IGasSupply _gasSupply;
    public AE86(IGasSupply gasSupply)
    {
        _gasSupply = gasSupply;
    }

    public string Run()
    {
        int accelerator = _gasSupply.GetGas();

        if (accelerator <= 0)
        {
            return "Won't start";
        }
        else if (accelerator < 100)
        {
            return "Slow";
        }
        else if (accelerator < 200)
        {
            return "Min";
        }
        else
        {
            return "Deja vu!";
        }
    }
}

現在我們為了測試可以去創建很多個用來測試的class

MSTest

在專案中新增一個測試項目選擇MSTest

然後在測試的程式中引用被測試的參考

在測試的專案建立用來測試的class 油門供給為0

  class GasSupplyLowerThanZero : IGasSupply
        {
            public int GetGas()
            {
                return 0;
            }
        }
 public class AE86Test
    {
        [TestMethod]
        public void GasSuplyLowerThan0_OK()
        {
            var car = new AE86(new GasSupplyLowerThanZero());
            var expected = "Won't start";
            var actual = car.Run();

            Assert.AreEqual(expected, actual);
        }

        class GasSupplyLowerThanZero : IGasSupply
        {
            public int GetGas()
            {
                return 0;
            }
        }
    }

我們預期如果供給油量是0車子無法發動,看一下測試結果是否如我們預期的

我們可以繼續創建多個測試用的油量供給

 class GasSupplyHigherThan200 : IGasSupply
        {
            public int GetGas()
            {
                return 210;
            }
        }

再寫一個新的測試Case

        [TestMethod]
        public void GasSuplyHigherThan200_OK()
        {
            var car = new AE86(new GasSupplyHigherThan200());
            var expected = "Fast";
            var actual = car.Run();

            Assert.AreEqual(expected, actual);
        }
現在發現我們的測試結果是有問題的,油量高於200應該是Deja vu!
目前到這邊一切都很順利,但有個問題是在我們單元測試中為了測試不同的情況需要不斷的去創建實現interface 的class,這些class的名稱都不是很好看

有沒有什麼辦法可以解決這個問題呢?

Mock

我們可以透過Mock來模擬測試的對象,特別是在複雜不好建立的對象的時候

.Net 單元測試中選擇Moq套件

現在我們可以用Mock直接創建實現interface的實例跳過創建class的步驟,可以把GasSupplyLowerThanZero、GasSupplyHigherThan200這些class都拿掉了,不用再專門創建用來測試的class

using Moq;

namespace Car.Tests
{
    [TestClass]
    public class AE86Test
    {
        [TestMethod]
        public void GasSuplyLowerThan0_OK()
        {
            var mock = new Mock<IGasSupply>();
            mock.Setup(gs => gs.GetGas()).Returns(0);
            var car = new AE86(mock.Object);
            var expected = "Won't start";
            var actual = car.Run();

            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void GasSuplyHigherThan200_OK()
        {
            var mock = new Mock<IGasSupply>();
            mock.Setup(gs => gs.GetGas()).Returns(200);
            var car = new AE86(mock.Object);
            var expected = "Deja vu!";
            var actual = car.Run();

            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void GasSuplyHigherThan100LowerThan200()
        {
            var mock = new Mock<IGasSupply>();
            mock.Setup(gs => gs.GetGas()).Returns(150);
            var car = new AE86(mock.Object);
            var expected = "Min";
            var actual = car.Run();

            Assert.AreEqual(expected, actual);
        }
    }
}

以上只是一個簡單的測試小例子,未來如果專案中有撰寫到單元測試後再整理成較為有系統的心得

Visited 85 times, 1 visit(s) today

Leave A Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *