單元測試Unit Test (二)─實例撰寫過程─面積計算功能

上一篇說明單元測試基本概念
這篇用實例寫單元測試,並拆解過程。

會從需求分析開始一步步做到單元測試

這裡直接拿KATA上的題目,該題為Level 6,L1最難~L8最簡單。
KATA─6 kyu TDD Area Calculations

DESCRIPTION:
Finish this kata with the unit tests as your only help!
Task:

  • Implement:
    Calculator.GetTotalArea()
  • Define the different shapes: `Square`, `Rectangle`, `Circle` and `Triangle`

第一步 需求分析

  1. 題目需求中,有哪些物件?
    Square, Rectangle, Circle, Triangle
  2. 題目要執行的功能Calculator.GetTotalArea()
    會將輸入的所有圖形面積加總。

第二步 畫類別圖 Class Diagram

  1. 從題目分析出有四個物件Square, Rectangle, Circle, Triangle
  2. 接著思考使用者如何用? 
    Answer : 
    使用者不用直接認識每個圖形,而是透過公開的介面IShape使用所有圖形。
    →使用介面,就不怕圖形增加、使用者要多認識一個圖形。
    →使用者不需要管圖形有哪些,從頭到尾只要使用IShape就可以。
    接著使用者需要透過Calculator.GetTotalArea()認識IShape。 
    .GetTotalArea()裡面參數要放圖形和圖形元素。 
    因此,使用者用Calculator使用IShape,透過IShape使用四個形狀。
  3. 畫類別圖
  • 類別圖工具:

方法ㄧ、用像drawio的工具拉出類別圖

方法二、可以在Hackmd、Notion用Mermaid語法畫圖(下面附Mermaid程式碼給大家)

classDiagram

class Rectangle{
	-length : double
	-width : double
	+Area() double
}

class Square{
	-length : double
	+Area() double
}

class Circle{
	-radius : double
	-pi=3.14159 : double
	+Area() double
}

class Triangle{
	-baseSide : double
	-height : double
	+Area() double
}

class IShape{
	<<interface>>
	+Area() double
}

IShape <|--Rectangle
IShape <|--Square
IShape <|--Circle
IShape <|--Triangle

class Calculator{
	+GetTotalArea(Ishape[] shapes ) double
}

Calculator ..> IShape : (uses a) Dependency

第三步 寫文件─驗收測試

照著Gherkin格式寫驗收測試。

  • Feature: 一句話帶出題目重點。
  • Scenario: [TestMethod]寫出每種被測試情境、例外狀況。
  • Given ( Arrange )前情題要、前置條件 : 描述在測試執行之前需要設定的環境或狀態。
  • And
  • And
  • When ( Act ) 做甚麼事、觸發條件: 描述觸發測試行為的操作或事件。
  • Then ( Assert ) 期望結果: 描述期望得到的結果。

以上三步驟的內容會放入README,
這邊附上README內容大家。以及Github上的呈現連結README─Area Calculation
下面也會教學如何開README,寫入文件。

# AreaCalulations

## 問題
Implement:  
```csharp
Calculator.GetTotalArea()
```
Define the different shapes: `Square`, `Rectangle`, `Circle` and `Triangle`

[Link](https://www.codewars.com/kata/tdd-area-calculations/csharp)

## 類別圖
```mermaid
classDiagram
class Rectangle {
  -double height
  -double width
  +Area() double
}

class Square{
   -double length
   +Area() double
}

class Circle{
   -double radius
   -double pi = 3.14159
   +Area() double
}

class Triangle{
   -double baseSide
   -double height
   +Area() double
}

class IShape {
  <<interface>>
  +Area() double
}

IShape <|-- Rectangle
IShape <|-- Square
IShape <|-- Circle
IShape <|-- Triangle

class Calculator{
   +GetTotalArea(IShape[] shapes) double
}

Calculator ..> IShape
```

## 驗收測試
```gherkin
Feature: Calculate the area of different shapes.
  Four shapes: Rectangle, Square, Circle, Triangle.

  Scenario: Calculate Rectangle
    Given There is a rectangle, height is 3 cm, and width is 5 cm
    When Calculate area
    Then The area is 15 square cm

  Scenario: Calculate Square
    Given There is a square, and length is 3 cm
    When Calculate area
    Then The area is 9 square cm

  Scenario: Calculate Circle
    Given There is a circle, and radius is 4 cm 
    When Calculate area
    Then The area is 50.27 square cm

  Scenario: Calculate Triangle
    Given There is a triangle, height is 4 cm, and base-side is 5 cm
    When Calculate area
    Then The area is 10 square cm

  Scenario: Calculate different shapes
    Given There is a rectangle, height is 3 cm, and width is 5 cm
    And There is a rectangle, height is 4 cm, and width is 8 cm
    And There is a square, and length is 3 cm
    And There is a circle, and radius is 4 cm
    And There is a triangle, height is 4 cm, and base-side is 5 cm
    When Calculate area
    Then The area is 116.27 square cm

  Scenario: Calculate shapes and rounded to two decimal places 四捨五入到小數第二位
    Given There is a rectangle, height is 3.251 cm, and width is 1 cm
    And There is a circle, and radius is 1 cm
    When Calculate area
    Then The area is 6.39 square cm

  Scenario: No shapes 
    Given There is no shape
    When Calculate area
    Then The area is 0 square cm
```

第四步 單元測試

完成文件後,開始寫單元測試。

上一篇認識單元測試的注意事項:

  1. 先讓測試通過,再來重構Refactor
  2. 用Github記錄每一步
    ✔一個單字記錄這次的意圖:
    Refactor (重構)
    Style (e.g.,重新命名、格式排版)
    Feature (寫功能)
    Fix (修bug)

(ㄧ)建立MSTest方案

  1. 新增MSTest方案
    步驟如上一篇單元測試工具─Visual Studio的MSTest
    1. MSTest方案,命名為"AeaCalculation"
    2. UnitTest專案,命名為"AreaOfShapeCalulationTests"
  2. 建立類別庫
    1. 在方案"AeaCalculation",按右鍵→[加入]→[新增專案]→選擇類別庫
    2. 類別庫,命名為"AreaOfShapeCalulations"
  3. UnitTest『參考』類別庫
    1. 按右鍵→[加入]→[專案參考]→勾選類別庫(AreaOfShapeCalculations)
      (如圖一)
    2. 成功將方案,分成"Unit Test測試"和"Production Code產品"兩個區塊。
  4. 建立Git,勾選README。(如圖二紅框)
  5. Git就會一次記錄下面這兩個歷程
Git記錄─新增 .gitattributes、.gitignore 和 README.md。
Git記錄─加入專案檔案。
圖一、讓UnitTest『參考』類別庫
圖二、建立Git

(二)將文件放進README

打開README(如圖三):

  1. 在[AreaCalculations方案]點右鍵→[在檔案總管中開啟資料夾]
  2. 在資料夾中找到README檔案
  3. 左鍵按住,拖曳進Visual Studio開啟
圖三、開啟README

將前面寫的需求、類別圖、驗收測試文件放進README。(如圖四)

在[Git變更]→[輸入訊息]→按[全部提交],就會有Git記錄。

Git記錄─README: 需求分析(類別圖)與驗收案例(gherkin)
圖四、放進README。Git變更並Commit。

(三)開始寫功能─

1️⃣計算長方形面積

將UnitTest1重新命名為"CalculateAreaTests"
搭配驗收測試文件,開始寫單元測試

驗收測試文件中的每個Scenario區塊,都會放進一個[TestMethod]

所以找到第一個Scenario: Calculate Rectangle
→將TestMethod的名稱改為"CalculateRectangleArea"

看他的Given-When-Then

  Scenario: Calculate Rectangle
    Given There is a rectangle, height is 3 cm, and width is 5 cm
    When Calculate area
    Then The area is 15 square cm

先看第一句Given

1. Given There is a rectangle, height is 3 cm, and width is 5 cm

There is a rectangle中文意思"有"一個長方形,代表要new出一個長方形物件。
→先輸入var rectangle = new Rectangle();
這時會看到Rectangle下面有紅色底線,因為我們並沒有一個名稱為Rectangle的類別(class)。
除了自己建立類別(class)之外,還有一個快速的方法:

游標放到紅色底線的Rectangle(如圖五),
按出現的燈泡、或是快捷鍵:Alt + Enter,
就會出現解決問題的方法!
我們這裡要建立class,因此選擇:「產生class “Rectangle”」

圖五、使用IDE快速動作(Quick Actions)中的燈泡

紅色波浪消失,代表沒有錯誤了。
下面也自動出現 class Rectangle(圖六)👍
 

圖六、使用燈泡後IDE自動建立class

再回來看驗收文件Given There is a rectangle, 後面的height is 3 cm, and width is 5 cm
一定要有長和寬才能組成一個長方形,因此可以將長和寬做為叫出Rectangle時一定要放入的參數,所以我們要使用建構式
→在Rectangle()裡面放入3,5,Rectangle出現紅色波浪,因為我們還沒建立建構式。

如前面利用IDE的快速方式(如圖七):
游標放到紅色底線的Rectangle,
按出現的燈泡、或是快捷鍵:Alt + Enter,
我們這裡要建立建構式,因此選擇:「在Rectangle中產生建構函式」
他也會一起產生欄位給我們。

圖七、使用IDE燈泡產生建構式

如下圖左,IDE自動在class Rectangle內生成欄位、建構式。
我們需要做的事剩下「重新命名」、「確認回傳型態」(如圖八)。

「重新命名」
建構式內的Rectangle(參數1, 參數2),有兩個參數,所以也需要兩個欄位來接。
在要改的名稱上,右鍵[重新命名],依測試文件命名為長和寬。
❗❗注意:不要直接改名稱,而要用IDE的重新命名功能。

為什麼要用IDE的重新命名功能呢?
因為你針對Y重新命名,IDE便會同時幫你調整在程式碼各處的Y。

「確認回傳型態」
IDE給我們的是int型態,但我們可能有小數點,因此將型態都改為double。

圖八、使用IDE的重新命名

2. When Calculate area
有長方形後要計算面積,就是題目需要的功能了Calculator.GetTotalArea()(被測試對象)。
所以我需要先建立Calculator物件(如圖九)。

圖九、IDE快速動作建立class Calculator

new了Calculator並建立好class後,我們就要使用方法GetTotalArea()。

這個方法首先要計算長方形,所以我們就放rectangle進去Calculator.GetTotalArea(rectangle)(如圖十)。
IDE快速建立方法,class Calculator就出現GetTotalArea()。
我們只要讓測試能先過了就好❗❗所以直接讓這個方法回傳我們要的答案:15。
並記得改回傳型態為double。

圖十、使用GetTotalArea()方法

3. Then The area is 15 square cm
TestMethod最後一步看最後結果是否等於預期結果(如圖十一)

方法一:
用關鍵字Assert.AreEqual(expected期待值, actual實際);

方法二:
安裝NuGet套件裡的FluentAssertions(有在上一篇說明單元測試基本認識),using FluentAssertions,然後用要被檢核的變數.Should().Be(期望的值);

圖十一、UnitTest的Assert

接著我們就來測試!

測試的步驟:在上一篇「認識單元測試
快捷鍵:按Ctrl+R, A (等於按住Ctrl去按R、全部放開去按A)
測試結果顯示在測試總管視窗。

讓測試總管視窗內的測試案例全部綠燈。

圖十二、測試全部綠燈

恭喜!你完成了一個TestMethod!

Git記錄─feat: 計算長方形面積。

2️⃣重構:寫出面積邏輯

剛剛我們在class Calculator中的GetTotalArea(),直接回傳15,並沒有寫出面積邏輯,現在就來重構這塊。

  1. 在class Rectangle裡面加上Area()方法,回傳長方形面積計算邏輯。
    因為我認為計算長方形面積是長方形自己的能力。
    (如圖十三)
  2. 因此將class Calculator中的GetTotalArea()回傳,改為回傳rectangle.Area()。
    (如圖十四)
  3. 執行測試。
    快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
圖十三、class Rectangle加上Area()方法
圖十四、改為回傳rectangle.Area()

現在將Rectangle類別搬移到類別庫"AreaOfShapeCalculations"。

  1. 將整個class Rectangle選取起來,按Alt+Enter→選擇[將類型移到Rectangle.cs]。(類似圖十五)
    ➡他會建立在UnitTest "AreaOfShapeCalculationTests"下面。
  2. 按住Rectangle.cs,拖移到類別庫"AreaOfShapeCalculations"再放開。
    →UnitTest下和類別庫下都會有Rectangle.cs。
  3. 將UnitTest下的Rectangle.cs刪掉。
  4. class Calculator同上面步驟,也要移動到"AreaOfShapeCalculations"。
    (如圖十五)
  5. 兩個class都移出去到類別庫中後,會看到CalculateAreaTests的Rectangle、Calculator都有紅色底線。錯誤訊息都寫缺了using。
    →在"CalculateAreaTests"最上面放using AreaOfShapeCalculations; 
    (如圖十六)
  6. 將Rectangle.cs的封裝internal改成public。
    Area()方法維持internal。
    因為只有Calculator會使用到rectangle.Area()。而Calculator和Rectangle是在同一個專案下。
    (如圖十七)
  7. 將Calculator.cs的所有封裝internal都改成public。
    (如圖十八)
  8. 執行測試。
    快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
圖十五、將類型移到Calculator.cs
圖十六、using AreaOfShapeCalculations
圖十七、將Rectangle.cs封裝internal改成public
圖十八、將Calculator.cs封裝internal改成public
Git記錄─refactor: 將 hard code 改用長方形面積邏輯。 
1. 將return15的15改放rectangle.Area()。 
2. 將類別搬移進AreaOfShapeCalculations(Production Code)

3️⃣下一個Scenario正方形

Scenario: Calculate Square
  Given There is a square, and length is 3 cm
  When Calculate area
  Then The area is 9 square cm

Scenario: Calculate Square
→將前一個Rectangle的[TestMethod] 整個複製,貼上成第二個[TestMehtod]。
→將名稱重新命名為"CalculateSquareArea"

接著來看Given-When-Then

寫Square的步驟跟Rectangle大同小異(如圖十九)我們來看看。

如圖十九、CalculateSquareArea

1.Given There is a square, and length is 3 cm

  1. new一個square,裡面的參數相較rectangle只需要放一個。
    因為長方形的長和寬可能會不一樣,正方形邊長必定一樣。
    var square = new Square(3);
  2. Square下方有紅色波浪,按Alt+Enter
    選擇[在新檔案中建立class Square]
    →建立出Square.cs
  3. 重新命名Square.cs的變數。(如圖二十)
  4. 將Square.cs移動到類別庫AreaOfShapeCalculations,封裝的internal改成public。
圖二十、重新命名Square.cs的變數

 

2. When Calculate area

  1. GetTotalArea()括號內的rectangle改成square
  2. ❗❗先求過測試,再來優化。
    所以我們複製rectangle的寫法,來寫square,用多型的特性。(如圖二十一)
  3. Area()紅色波浪處Alt+Enter→選取產生方法→在class Square自動產生方法Area()。
  4. 去到Square.cs,Area() 修改return為正方形面積計算邏輯(如圖二十二)。
圖二十一、複製GetTotalArea把參數改成能讓Square通過
圖二十二、回傳正方形面積邏輯

3. Then The area is 9 square cm

將Assert內的期望值從15改為9。

執行測試。
快捷鍵:按Ctrl+R, A。讓測試全部綠燈。

Git記錄─feat: 實作正方形的面積計算。

這回合結束🎉

4️⃣將87%像的兩個GetTotalArea方法重構

  1. 將Rectangle改成IShape,並產生介面
  2. 回傳改成shape.Area();
  3. 在IShape中產生Area()方法。
    (如圖二十三)
  4. 實作IShape:
    在class Rectangle後面 加上→:IShape
    出現紅色波浪(因為實作IShape就要實作Area())
  5. 我們將Area()改成public就會符合IShape要的Area()了。
    (如圖二十四)
  6. 重複上面的步驟
    回到Calculator拿掉Square的GetTotalArea。
    只留IShape。
  7. 實作IShape:
    在class Square後面 加上→:IShape
    出現紅色波浪(因為實作IShape就要實作Area())
  8. 執行測試。
    快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
圖二十三、產生介面IShape
圖二十四、在Rectangle實作介面

補充:為什麼使用介面?

我們在不同形狀都需要同一個方法GetTotalArea計算面積功能,
但正方形、長方形、三角形計算面積的方式卻不同。
這時候就要使用介面。

讓class Rectangle和class Square、其他形狀…都實作介面,
只要實作介面,就一定要建立介面規定的計算面積功能,
各自建立後再去改成長方形面積計算方式、三角形面積計算方式…
就可以讓使用者統一使用該介面,不用管背後是如何運作的,使用者可以得到不同形狀的面積。

Git記錄─refactor: 擷取IShape介面。

5️⃣

Git記錄─refactor: 針對測試個案擷取方法。
1. TestInitialize 
2. TotalAreaShouldBe

Git記錄─feat: 實作圓形面積計算邏輯。調整測試個案中PI的小數進位問題。

Git記錄─refactor: 把圖形統一放進新增的Shape資料夾

Git記錄─feat: 實作三角形面積計算邏輯

Git記錄─feat: 支援多圖形面積計算邏輯 
1. 使用params關鍵字將多參數(圖形)放入陣列 
2. 實作多圖形面積加總

Git記錄─feat: 補上最後兩種測試。四捨五入、沒形狀要計算情況。

Git記錄─style: 排版 format

 

謝謝觀看,此為新手的學習筆記整理,若有錯誤,煩請指正🙏