上一篇說明單元測試基本概念,
這篇用實例寫單元測試,並拆解過程。
會從需求分析開始一步步做到單元測試
這裡直接拿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`
第一步 需求分析
- 題目需求中,有哪些物件?
Square
,Rectangle
,Circle
,Triangle
- 題目要執行的功能
Calculator.GetTotalArea()
會將輸入的所有圖形面積加總。
第二步 畫類別圖 Class Diagram
- 從題目分析出有四個物件
Square
,Rectangle
,Circle
,Triangle
- 接著思考使用者如何用?
Answer :
使用者不用直接認識每個圖形,而是透過公開的介面IShape使用所有圖形。
→使用介面,就不怕圖形增加、使用者要多認識一個圖形。
→使用者不需要管圖形有哪些,從頭到尾只要使用IShape就可以。
接著使用者需要透過Calculator.GetTotalArea()
認識IShape。.GetTotalArea()
裡面參數要放圖形和圖形元素。
因此,使用者用Calculator使用IShape,透過IShape使用四個形狀。 - 畫類別圖
- 類別圖工具:
方法ㄧ、用像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
```
第四步 單元測試
完成文件後,開始寫單元測試。
上一篇認識單元測試的注意事項:
- 先讓測試通過,再來重構Refactor
- 用Github記錄每一步
✔一個單字記錄這次的意圖:
Refactor (重構)
Style (e.g.,重新命名、格式排版)
Feature (寫功能)
Fix (修bug)
(ㄧ)建立MSTest方案
- 新增MSTest方案
步驟如上一篇單元測試工具─Visual Studio的MSTest。- MSTest方案,命名為"AeaCalculation"
- UnitTest專案,命名為"AreaOfShapeCalulationTests"
- 建立類別庫
- 在方案"AeaCalculation",按右鍵→[加入]→[新增專案]→選擇類別庫
- 類別庫,命名為"AreaOfShapeCalulations"
- 讓UnitTest『參考』類別庫
- 按右鍵→[加入]→[專案參考]→勾選類別庫(AreaOfShapeCalculations)
(如圖一) - 成功將方案,分成"Unit Test測試"和"Production Code產品"兩個區塊。
- 按右鍵→[加入]→[專案參考]→勾選類別庫(AreaOfShapeCalculations)
- 建立Git,勾選README。(如圖二紅框)
- Git就會一次記錄下面這兩個歷程
Git記錄─新增 .gitattributes、.gitignore 和 README.md。
Git記錄─加入專案檔案。
(二)將文件放進README
打開README(如圖三):
- 在[AreaCalculations方案]點右鍵→[在檔案總管中開啟資料夾]
- 在資料夾中找到README檔案
- 左鍵按住,拖曳進Visual Studio開啟
將前面寫的需求、類別圖、驗收測試文件放進README。(如圖四)
在[Git變更]→[輸入訊息]→按[全部提交],就會有Git記錄。
Git記錄─README: 需求分析(類別圖)與驗收案例(gherkin)
(三)開始寫功能─
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”」
紅色波浪消失,代表沒有錯誤了。
下面也自動出現 class Rectangle(圖六)👍
再回來看驗收文件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自動在class Rectangle內生成欄位、建構式。
我們需要做的事剩下「重新命名」、「確認回傳型態」(如圖八)。
「重新命名」
建構式內的Rectangle(參數1, 參數2),有兩個參數,所以也需要兩個欄位來接。
在要改的名稱上,右鍵[重新命名],依測試文件命名為長和寬。
❗❗注意:不要直接改名稱,而要用IDE的重新命名功能。
為什麼要用IDE的重新命名功能呢?
因為你針對Y重新命名,IDE便會同時幫你調整在程式碼各處的Y。
「確認回傳型態」
IDE給我們的是int型態,但我們可能有小數點,因此將型態都改為double。
2. When Calculate area
有長方形後要計算面積,就是題目需要的功能了Calculator.GetTotalArea()
(被測試對象)。
所以我需要先建立Calculator物件(如圖九)。
new了Calculator並建立好class後,我們就要使用方法GetTotalArea()。
這個方法首先要計算長方形,所以我們就放rectangle進去Calculator.GetTotalArea(rectangle)
(如圖十)。
IDE快速建立方法,class Calculator就出現GetTotalArea()。
我們只要讓測試能先過了就好❗❗所以直接讓這個方法回傳我們要的答案:15。
並記得改回傳型態為double。
3. Then The area is 15 square cm
TestMethod最後一步看最後結果是否等於預期結果(如圖十一)
方法一:
用關鍵字Assert.AreEqual(expected期待值, actual實際);
方法二:
安裝NuGet套件裡的FluentAssertions(有在上一篇說明單元測試基本認識),using FluentAssertions
,然後用要被檢核的變數.Should().Be(期望的值);
接著我們就來測試!
測試的步驟:在上一篇「認識單元測試」
快捷鍵:按Ctrl+R, A (等於按住Ctrl去按R、全部放開去按A)
測試結果顯示在測試總管視窗。
讓測試總管視窗內的測試案例全部綠燈。
恭喜!你完成了一個TestMethod!
Git記錄─feat: 計算長方形面積。
2️⃣重構:寫出面積邏輯
剛剛我們在class Calculator中的GetTotalArea(),直接回傳15,並沒有寫出面積邏輯,現在就來重構這塊。
- 在class Rectangle裡面加上Area()方法,回傳長方形面積計算邏輯。
因為我認為計算長方形面積是長方形自己的能力。
(如圖十三) - 因此將class Calculator中的GetTotalArea()回傳,改為回傳rectangle.Area()。
(如圖十四) - 執行測試。
快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
現在將Rectangle類別搬移到類別庫"AreaOfShapeCalculations"。
- 將整個class Rectangle選取起來,按Alt+Enter→選擇[將類型移到Rectangle.cs]。(類似圖十五)
➡他會建立在UnitTest "AreaOfShapeCalculationTests"下面。 - 按住Rectangle.cs,拖移到類別庫"AreaOfShapeCalculations"再放開。
→UnitTest下和類別庫下都會有Rectangle.cs。 - 將UnitTest下的Rectangle.cs刪掉。
- class Calculator同上面步驟,也要移動到"AreaOfShapeCalculations"。
(如圖十五) - 兩個class都移出去到類別庫中後,會看到CalculateAreaTests的Rectangle、Calculator都有紅色底線。錯誤訊息都寫缺了using。
→在"CalculateAreaTests"最上面放using AreaOfShapeCalculations;
(如圖十六) - 將Rectangle.cs的封裝internal改成public。
Area()方法維持internal。
因為只有Calculator會使用到rectangle.Area()。而Calculator和Rectangle是在同一個專案下。
(如圖十七) - 將Calculator.cs的所有封裝internal都改成public。
(如圖十八) - 執行測試。
快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
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大同小異(如圖十九)我們來看看。
1.Given There is a square, and length is 3 cm
- new一個square,裡面的參數相較rectangle只需要放一個。
因為長方形的長和寬可能會不一樣,正方形邊長必定一樣。
→var square = new Square(3);
- Square下方有紅色波浪,按Alt+Enter
選擇[在新檔案中建立class Square]
→建立出Square.cs - 重新命名Square.cs的變數。(如圖二十)
- 將Square.cs移動到類別庫AreaOfShapeCalculations,封裝的internal改成public。
2. When Calculate area
- GetTotalArea()括號內的rectangle改成square
- ❗❗先求過測試,再來優化。
所以我們複製rectangle的寫法,來寫square,用多型的特性。(如圖二十一) - Area()紅色波浪處Alt+Enter→選取產生方法→在class Square自動產生方法Area()。
- 去到Square.cs,Area() 修改return為正方形面積計算邏輯(如圖二十二)。
3. Then The area is 9 square cm
將Assert內的期望值從15改為9。
執行測試。
快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
Git記錄─feat: 實作正方形的面積計算。
這回合結束🎉
4️⃣將87%像的兩個GetTotalArea方法重構
- 將Rectangle改成IShape,並產生介面
- 回傳改成shape.Area();
- 在IShape中產生Area()方法。
(如圖二十三) - 實作IShape:
在class Rectangle後面加上→:IShape
出現紅色波浪(因為實作IShape就要實作Area()
) - 我們將Area()改成public就會符合IShape要的Area()了。
(如圖二十四) - 重複上面的步驟
回到Calculator拿掉Square的GetTotalArea。
只留IShape。 - 實作IShape:
在class Square後面加上→:IShape
出現紅色波浪(因為實作IShape就要實作Area()
) - 執行測試。
快捷鍵:按Ctrl+R, A。讓測試全部綠燈。
補充:為什麼使用介面?
我們在不同形狀都需要同一個方法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
謝謝觀看,此為新手的學習筆記整理,若有錯誤,煩請指正🙏