0x00 前言 關於TDD測試驅動開發的文章已經有很多了,但是在游戲開發尤其是使用Unity3D開發游戲時,卻聽不到特別多關於TDD的聲音。那麼本文就來簡單聊一聊TDD如何在U3D項目中使用以及如何使用U3D 5.3.X之後版本已經集成的單元測試模塊Editor Test Runner。 0x01 ...
0x00 前言
關於TDD測試驅動開發的文章已經有很多了,但是在游戲開發尤其是使用Unity3D開發游戲時,卻聽不到特別多關於TDD的聲音。那麼本文就來簡單聊一聊TDD如何在U3D項目中使用以及如何使用U3D 5.3.X之後版本已經集成的單元測試模塊Editor Test Runner。
0x01 你好,TDD
TDD,測試驅動開發改變了我們常見的工作流程,不要求先寫邏輯代碼,反而要求先完成測試代碼。待測試代碼完成之後,我們再將目光轉移到邏輯代碼,根據測試的要求,完成邏輯代碼,使之能夠通過經過拆分後粒度已經很小的測試。這樣做有什麼好處呢?
- 要將任務拆分成可測試的各個測試用例,這就要求我們在完成邏輯代碼時要將代碼的功能儘可能細分,換句話說就是讓一個類/方法只負責單一責任,當這個類/方法需要承擔其他類型/方法的責任的時候,就需要分解這個類/方法。這就迫使我們要把程式設計成易於調用和可測試的,即迫使我們解除軟體中的耦合。
- 更加適合應對需求的經常性變更。身處游戲開發行業的從業人員都不能否認的一點便是游戲開發中需求變更是一件不可避免甚至是必不可少的事情,而基於測試驅動開發的另一個好處便是一旦因為需求變更而出現bug,能夠很快的發現,進而解決問題。
- 單元測試是一種無價的文檔,它是展示方法或類如何使用的最佳文檔。這份文檔是可編譯、可運行的,並且它保持最新,永遠與代碼同步。
0x02 流程,驅動
為了進行TDD測試驅動開發,我們需要瞭解TDD的流程或者說技巧,大體上可以將其步驟簡單的歸納為:紅燈->綠燈->重構。
但是測試是什麼?測試是誰執行的?測試又是如何驅動開發的呢?下麵我們就通過一個小例子來聊一聊這個問題。
程式是什麼?簡單的說就是一段有預期輸出的代碼。我們可以執行這段程式,並獲得程式的輸出。而所謂的測試,便是這樣的一段程式,它會自動調用執行另一段需要被測試的代碼(在這裡我們依靠一些測試框架來實現,例如針對C#的測試框架NUnit),並且根據輸出的可見結果來驗證某些假設是否成立,例如輸出的結果證明假設成立,則測試通過。
簡單的瞭解了測試之後,我們通過一個小例子來看看測試驅動開發的思路和流程是怎樣的,並且一探“驅動”的具體含義。
紅燈
下麵,我們就利用NUnit來編寫我們的第一個測試,來看看測試是如何驅動開發的:
//測試被攻擊之後傷害數值是否和預期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
HpComp health = new HpComp();
health.currentHp = 100;
health.TakeDamage(50);
Assert.AreEqual(50f, health.currentHp);
}
首先可以看到測試代碼的方法名很長,而且測試名中還包括下劃線來保證我們不會漏掉關於這個測試的重要信息(被測試的方法_測試進行的條件_預期結果),因為在編寫測試代碼時,可讀性是重要的考量之一。
繼續看測試代碼,我們現在測試的類是HpComp,它包括一個欄位currentHp保存了現在的血量值,還有一個方法TakeDamage。最開始我們會將currentHp初始化為100,之後調用TakeDamage方法,最後使用NUnit的Assert類所提供的靜態方法AreEqual來斷言假設是否成立,也即判斷是否通過測試。
此時,由於我們還沒有聲明一個叫HpComp的類來處理和血量相關的邏輯,也沒有一個叫currentHp的欄位來保存現在的血量,更沒有一個叫TakeDamage的方法,因此我們運行這個測試的結果便是失敗。換言之,我們現在處於紅燈階段。
綠燈
測試寫完了,此時是紅燈,而此時將這個紅燈變成綠燈的要求,便驅使著我們進行開發。所幸的是,我們要開發的內容,已經在測試中體現了出來:
- 實現一個叫做HpComp的類
- 為HpComp增加一個欄位currentHp,用來保存現在的血量
- 實現一個叫做TakeDamage的方法,而在這個測試中事實上只要求TakeDamage方法將currentHp的值變成50即可。
只要滿足這3點,我們就可以很輕易的使紅燈變成綠燈。所以,為了滿足測試條件,我們可以十分簡單粗暴的寫出如下的代碼:
public class HpComp
{
public float currentHp;
public void TakeDamage(float damage)
{
this.currentHp = 50f;
}
}
好了,在上面的測試代碼中只要調用TakeDamage方法,currentHp的值便被設置為了50,和斷言中的預期符合,因此測試通過,狀態也由紅燈變成了綠燈。當然,我們簡單的實現就通過了第一個測試,此時如果有優化代碼的需求,我們就需要對代碼進行重構,使得代碼更加乾凈。
再來幾次
我們的第一個測試用例驅動開發出的代碼顯然滿足了第一個測試的需求,但是如果我們重新回到原點,並且思考一下除了滿足第一個測試中提供的數據,我們的代碼還能做什麼,如果換一個測試條件結果會變得怎樣呢?
我們來完成一個新的測試:
//測試被攻擊之後傷害數值是否和預期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual2()
{
HpComp health = new HpComp();
health.currentHp = 150;
health.TakeDamage(10);
Assert.AreEqual(140f, health.currentHp);
}
這是一個新的測試(暫時叫做測試2),這就意味著TakeDamage方法除了通過第一個測試之外,還必須通過這個新的測試2。此時,我們最初的TakeDamage的實現,顯然無法通過測試2,因此測試2是紅燈狀態。
這也就是說,隨著我們的測試增加,會帶來更多的預期和要求,從而驅動我們開發出滿足這些預期和要求的代碼來。隨著測試2的出現,我們將TakeDamage方法編程了下麵這個樣子:
public void TakeDamage(float damage)
{
this.currentHp -= damage;
}
這樣,它不僅通過了測試1,同時也通過了測試2。
但是如果我們重覆上面的流程,提出更多的測試呢?也許我們還會發現TakeDamage方法可能會出現越界的情況,或者是輸入不合法的情況等等。當然,這些都可以通過更多的測試來驅動我們開發出更健康的代碼。
TDD流程小結
通過上面的小例子,我們可以看到TDD的流程或者說開發技巧並不難理解:
- 編寫一個會失敗的測試,以證明產品中的代碼或功能的缺陷。
- 編寫符合測試預期的代碼。
- 重構代碼,如果測試通過了,就可以選擇重構,目標是使代碼的可讀性更強、減少重覆代碼。如果不重構,則可以開始編寫下一個測試,即重覆第4步。
- 重覆以上過程。
0x03 問題,方案
由於游戲開發和傳統軟體開發之間的差異,因此在開發游戲的過程中編寫單元測試,會面臨兩個主要的問題:
1.游戲開發中會涉及到很多的I/O操作處理,以及視覺和UI的處理,而這個部分是單元測試中比較難以處理的部分。
2.具體到使用Unity3D開發游戲,我們自然而然的希望能夠將測試的框架集成到Unity3D的編輯器中,這樣更加容易操作。
針對問題1,由於對I/O處理以及UI視覺方面的操作比較難以實施單元測試,所以我們單元測試的主要對象是邏輯操作以及數據存取的部分。
針對問題2,Unity5.3.x已經在editor中集成了測試模塊。該測試模塊依托了NUnit框架(NUnit是一個單元測試框架,專門針對於.NET來寫的.其實在前面有JUnit(Java),CPPUnit(C++),他們都是xUnit的一員.最初,它是從JUnit而來.U3d使用的版本是2.6.4)。
而且除了Unity5.3.x自帶的單元測試模塊之外,Unity官方還推出了一款測試插件Unity Test Tool(基於NSubstitute)。
0x04 實踐,U3D中的單元測試
在Untiy編輯器中寫單元測試:
編寫單元測試用例時,使用的主要是Unity Editor自帶的單元測試模塊,因此單元測試是基於NUnit框架的。
這就要求編寫單元測試時,要引入NUnit.Framework命名空間,且單元測試類要加上[TestFixture]屬性,單元測試方法要加上[Test]屬性,並將測試用例的文件放在Editor文件夾下。
測試用例的編寫結構要遵循3A原則,即Arrange, Act, Assert。
即先要設置測試環境,例如實例化測試類,為測試類的欄位賦值。
之後操作對象,即寫測試的行為。
最後是斷言某件事情是預期的,即判斷是否通過測試。
下麵是一個例子:
using UnityEngine;
using System.Collections;
using NUnit.Framework;
[TestFixture]
public class HpCompTests
{
//測試被攻擊之後傷害數值是否和預期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
HpComp health = new HpComp();
health.currentHp = 100;
health.TakeDamage(50);
Assert.AreEqual(50f, health.currentHp);
}
}
完成之後,我們就可以打開Unity 5.3.x中集成的單元測試模塊來進行自動化測試了。
好了,本文到此就暫時打住了,之後有新的體驗和想法,還會繼續這個話題的總結,也歡迎各位討論。