關於TDD測試驅動開發的文章已經有不少了,可是在遊戲開發尤爲是使用Unity3D開發遊戲時,卻聽不到特別多關於TDD的聲音。那麼本文就來簡單聊一聊TDD如何在U3D項目中使用以及如何使用U3D 5.3.X以後版本已經集成的單元測試模塊Editor Test Runner。html
TDD,測試驅動開發改變了咱們常見的工做流程,不要求先寫邏輯代碼,反而要求先完成測試代碼。待測試代碼完成以後,咱們再將目光轉移到邏輯代碼,根據測試的要求,完成邏輯代碼,使之可以經過通過拆分後粒度已經很小的測試。這樣作有什麼好處呢?編程
爲了進行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的方法,所以咱們運行這個測試的結果即是失敗。換言之,咱們如今處於紅燈階段。單元測試
測試寫完了,此時是紅燈,而此時將這個紅燈變成綠燈的要求,便驅使着咱們進行開發。所幸的是,咱們要開發的內容,已經在測試中體現了出來:測試
只要知足這3點,咱們就能夠很輕易的使紅燈變成綠燈。因此,爲了知足測試條件,咱們能夠十分簡單粗暴的寫出以下的代碼:優化
public class HpComp { public float currentHp; public void TakeDamage(float damage) { this.currentHp = 50f; } }
好了,在上面的測試代碼中只要調用TakeDamage方法,currentHp的值便被設置爲了50,和斷言中的預期符合,所以測試經過,狀態也由紅燈變成了綠燈。固然,咱們簡單的實現就經過了第一個測試,此時若是有優化代碼的需求,咱們就須要對代碼進行重構,使得代碼更加乾淨。this
咱們的第一個測試用例驅動開發出的代碼顯然知足了第一個測試的需求,可是若是咱們從新回到原點,而且思考一下除了知足第一個測試中提供的數據,咱們的代碼還能作什麼,若是換一個測試條件結果會變得怎樣呢?
咱們來完成一個新的測試:spa
//測試被攻擊以後傷害數值是否和預期值相等 [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的流程或者說開發技巧並不難理解:
因爲遊戲開發和傳統軟件開發之間的差別,所以在開發遊戲的過程當中編寫單元測試,會面臨兩個主要的問題:
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)。
編寫單元測試用例時,使用的主要是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中集成的單元測試模塊來進行自動化測試了。