2015-05-18

[心得文] 自動測試與TDD實務開發 Day1

[前情提要]
其實半年前我就想報名這堂課,不過可惜最後因為幾個負面思考而打消了念頭,當時在心中埋下了一顆悔恨的種子,告訴自己說,下次再有這個機會請不要錯過。
終於,今年盼到了第三梯的課程,雖然價格提高了4 =.= ,不過還是自己能夠負擔的範圍啦,當然就報囉。
剛好今年也給了自己一些明確的目標:
1.     2015Q4 要在公司教授Unit Testing的課程:
為了要教別人,所以先來觀摩大神91哥是怎麼教學員的XD
果然Lab中的幾個例子都很務實,也給了我一些方向。
2.     Team導入完整的Continuous Delivery Solution
為了使CD更完整,Unit Testing就不能缺席,我一定會建置出一個方便的Solution讓同事們能夠無痛升級。

於是呢,獲得老婆的諒解之後,踏上了學習之路!


[我們的痛?]
91在課程的一開始,先分組讓大家自我介紹,請大家分享一下為什麼要來上這堂課。我覺得這是個很好的開始,要解決任何問題之前,請先停下來思考:問題是什麼?釐清問題之後,大家的學習會更努力更有效率。
歸納當天學員們的問題,不外乎就是:
1.     想了解Unit Testing & TDD
2.     實務上怎麼導入?

以我自己為例,我為什麼想要學這堂課的原因是:
1.     我要導入Unit Testing至我們Team,希望能學到實務的例子,提供簡單的架構,降低同事們的學習門檻。
2.     我要教這堂Unit Testing,所以來學習91是如何教學員。

實務上開發常見的問題有那些呢?我想身為一個Developer,應該對這些問題都耳熟能詳,但是卻無法有效解決:

  • 測試環境無法準備
  • 不曉得Developer交付的code是否正確?
  • Side Effects
  • 跟其他Team一起平行開發中,過程中必須相互等待。

其實我們就能透過Unit Test解決上述問題。
1.     測試環境無法準備????
有些系統是屬於外部系統,他們的整合日期可能不是我們這個Team這個層級就能決定的,但是開發不能等他們好再來測,此時我們可以使用Mock Framework來協助我們克服這個問題。

2.     不曉得Developer交付的code是否正確?
交付code即附上Unit Test。結果清楚明瞭。未來若整合進CD,我們可以在建置後就能知道code是否正確,比起問題要到QA才發現還早多了。Bug是需要提早發現及早治療的。

3.     Side Effects
儘管有Code Review。大部分最難防的還是Side Effects吧!而完整的Unit Test就能幫大家補上這一塊,改完code隨時執行Unit Test。有沒有改東壞西馬上就知道結果。
遙想當初我想要Refactor某個Feature,卻因為沒有Unit Test,也沒有QA可以驗證而放棄。如果有Unit Test就會增加我們Developercode的自信心吧!

4.     跟其他Team一起平行開發中,過程中必須相互等待。
其實這問題跟第一點很像,但是更有可能以design的方式就能解決。體會到Unit Test的魅力之後,自然而然會想寫出可以測試的程式(Testability),在寫code時自然就會考量到Isolated關注點的問題,盡量將Dependency隔離開。最後的實務上就能以Mock Framework輕鬆隔離相依性,只測該測的地方,讓Unit Test變得簡單,簡單才會有人想用。

這條路上一定會被問到:
如果寫Unit Test,在開發上會增加多少時間?
這讓我想到之前讀”The Art of Unit Testing with Examples in .NET”這本書時,作者以他本身在大型企業的案例作為例子:





Developer開發的時間雖然變成2倍,但是整體的時間是下降的,更重要的是,Bug的數量很顯著的下降。這就是Unit Test的威力。

還有一句話讓我很有感:
面對未知的問題,不要用猜的,用測的最準!!!


[基礎概念]
什麼是Unit Test
幾點定義:
1.     最小的測試單位
2.     外部相依性為零
3.     沒有商業邏輯
4.     Test Case彼此的相依性為零
5.     一個Test Case一次只測一件事

其中最重要的兩點就是 2 & 5
只要把握這些特性,就能寫出有效的Unit Test
如果Test Case測了兩件事,代表測試失敗時會有兩種可能!

3A原則 Arrange, Act, and Assert
這裡學到幾個tips
1.     面對Unit Testing的初學者,我們在導入時可以將Arrange, Act, and Assert的結構用註解隔開。
2.     Naming時也請習慣使用target, expected, and actual
3.     Assert的位置是(expected, actual) 不要搞錯位置囉!
以上作法能夠幫助Developer清楚原則與用法。

Unit Test的特性:FIRST
Fast, Independent, Repeatable, Self-Validating and Timely

(心得) Repeatable Idempotent來解釋會更有說服力
Idempotent : 無論執行幾次,結果都是相同的。

[AreEqual v.s. AreSame]
原則:(MsTest, 但我想NUnit應該也差不多)
1.     比較value type的值用AreEqual()
2.     比較reference typememory位置是否相同用AreSame()
3.     比較 Collection 可用CollectionAssert
4.     想測是否會有Exception請用[ExpectedException],切記不要自己寫try catch
Exception一定要測如果是Scenario1就預期有ExceptionCase
5.     .Net String Object的特性:String雖是Reference Type,但是有實作StringPool的緣故,所以如果是同一個值將會指到同一塊Memory,所以也擁有value type的特性。

[如何解決相依性]
接下來就進入實務上最常碰到的問題了,也開始進入導入上所面臨的困難。
Solution :

  • 單一職責
  • 依賴Interface
  • 依賴注入
  • 利用Mock Framework

1.     單一職責
Design上就盡量讓一個Class,一個Method負責單一的職責。從這裡盡量降低一些Dependency

2.     依賴Interface
我們要從使用端的角度去定義Interface,從相依的Class中抽取出能共同使用的Interface

3.     依賴注入
這邊要想個法子讓測試程式可以替換掉有相依性的production code instance
91介紹了 (1) Constructor Injection & (2) Parameter Injection
我記得之前書上還有提到(3) Factory Method 以及(4)Extract and Override
下次再補上

4.     利用Mock Framework
自己去寫上述測試程式是很累人的,也會降低Developer想寫Unit Test的意願,此時Mock Framework就能幫助大家很簡單地寫出可避開相依性的測試程式。
這次使用的NSubstitute真的很好用,也不像Rhino違反了3A原則。我們可以透過Substitute.For<T>產生stub, 再透過xxx.TestMethod1.Return(R) 取得stub預期會回傳的R (Stub 關心的是回傳值)
若是想要測Mock (與外部的互動) 也能使用xxx.Received().ExpectedMethod() 知道預期是否有呼叫ExpectedMethod() 或是 DidNotReceived()驗證不被呼叫的情況。(切記,一次只測一件事,如果要測Mock,只要驗證是否有被呼叫即可。)

91的經驗:大部分都是測Stub, Mock的比例大約10%

[PublicMethod該不該測]

不用針對它特別測!!!
但是得透過Public Method測到它

不要為了測試而測試,想想需求吧。非PublicMethod一定是在哪裡被PublicMethod使用到。
Code Coverage的建議
1.     檢視Test Case是否包含最主要的情境
2.     檢視Test Case是否包含曾經出錯的情境
3.     檢視是否有不必要的code或是缺少對應的Test Case
4.     相對比例盡量只能往上,不要往下

[Internal該如何測試]
在實務上我們有些Class是設定成Internal的,測試程式可能無法取得。
Solution :
1.     AssemblyInfo.cs 加上 [assembly:InternalsVisibleTo(“XXX.Test”)]
2.     AssemblyInfo.cs 加上 [assembly:InternalsVisibleTo(“DynamicProxyGenAssembly2”)]
要給DynamicProxyGenAssembly2看得見才能動態建立stub/mock

[其他心得]
1. TDD導入與推廣的原則
(i) 如何讓Member不要太過麻煩就能做
(ii)如何盡量不影響現有production code

2. 不要為了測試而去切Interface
要反過來問需求,我們是為了彈性才去切Interface

3. .NET Framework的Object不建議用Interface去抽換

對這堂課有興趣的朋友們
可以關注一下SkillTree什麼時候要推出第四梯的課程喔
自動測試與 TDD 實務開發(使用C#) 第三梯

系列文章
[心得文] 自動測試與TDD實務開發 Day2

0 意見: