若是您今天正在編程,那麼您極可能據說過單元測試或測試驅動的開發過程。我尚未遇到一個既沒有據說過又沒有據說過單元測試並不重要的程序員。在隨意的討論中,大多數程序員彷佛認爲單元測試很是重要。java
可是,當我開始使用代碼並問「單元測試在哪裏?」時,我獲得了一個徹底不一樣的故事。我最近在網上問個人程序員朋友爲何不這樣作,以及爲何其餘程序員不這樣作呢?不要編寫單元測試。當我問程序員或IT經理一樣的問題時,我常常聽到的第一答案是:「我沒有時間」或相似的問題。一般會出現這樣的論點,即便用單元測試編寫應用程序要比不使用單元測試編寫時間長20%,而且「咱們受到時間限制」。程序員
個人建議–當咱們嘗試解決時間不足的問題時,也許咱們能夠在娛樂性上作出一些貢獻。編程
我正在爲一個應用程序設計原型,該應用程序將容許用戶輸入有關房屋裝修項目的信息,而後與朋友共享該項目的材料和工具信息。而後,朋友能夠承諾貸款或購買項目中所需的一些材料或工具。基本上是用於家庭裝修項目的「登記處」。promise
測試將在採用Project對象的方法上進行,遍歷該項目的工具列表以查看該工具是否已經被承諾,並建立一個未被承諾的工具列表。而後,它將把該列表傳遞給將查詢每一個工具當前價格的服務。框架
原型是用Grails完成的,可是咱們將用Java編寫此方法:編程語言
public List<Tool> neededToolList(Project project) { final List<Tool> retList = new ArrayList<>(); if (project.getTools() == null || project.getTools().isEmpty()) { return retList; } for (Tool tool : project.getTools()) { if (!tool.getPromise().isPromised()) { retList.add(tool); } } List<Tool> tools = lookupService.updateToolList(retList); return tools; }
單個單元測試可能相似於:工具
@Test public void testNeededToolList() { Tools _instance = new Tools(); Project project = new Project(); Promise promise = new Promise(); promise.setProject(project); promise.setPromised(false); Promise promise2 = new Promise(); promise2.setProject(project); promise2.setPromised(true); List<Tool> tools = new ArrayList<>(); List<Tool> lookupTools = new ArrayList<>(); Tool tool1 = new Tool(); tool1.setName("table saw"); tool1.setStoreId("T001"); tool1.setPromise(promise); tools.add(tool1); lookupTools.add(tool1); Tool tool2 = new Tool(); tool2.setName("pneumatic nail guns"); tool2.setStoreId("T027"); tool2.setPromise(promise2); tools.add(tool2); project.setTools(tools); List<Tool> mockedTools = new ArrayList<>(); Tool mockedTool1 = new Tool(); mockedTool1.setPromise(promise); mockedTool1.setName("table saw"); mockedTool1.setStoreId("T001"); mockedTool1.setPrice(129.0); mockedTools.add(mockedTool1); lookupService = Mockito.mock(LookupServiceImpl.class); Mockito.when(lookupService.updateToolList(lookupTools)).thenReturn(mockedTools); _instance.setLookupService(lookupService); List<Tool> returnedTools = _instance.neededToolList(project); assertTrue(returnedTools.size() == 1); for(Tool tool : returnedTools) { assertEquals(129.0, tool.getPrice(), 0.01); } }
這是一個簡單的測試,而且只有一個。須要針對幾種狀況編寫測試,例如空值。例如,若是StoreID不存在怎麼辦?性能
在以前的文章中,我已經介紹了個人好朋友Groovy編程語言。讓咱們看看是否能夠進行Groovy測試。單元測試
Groovy帶來了許多語法上的捷徑,這些捷徑有助於加快編寫代碼(包括測試)的速度。讓咱們看一下在Groovy中重寫該測試的可能方法。學習
class GroovyToolsTest extends GroovyTestCase { def lookupService = [ updateToolList : {List<Tool> toolList -> toolList.each { if(it.storeId == "T001") { it.price = 129.0 } } return toolList } ] as LookupService void testNeededToolList() { def _instance = new Tools() def project = new Project() project.tools = [ new Tool(name: "table saw", storeId: "T001", promise: new Promise(project: project, promised: false)), new Tool(name: "pneumatic nail guns", storeId: "T027", promise: new Promise(project: project, promised: true)) ] _instance.lookupService = lookupService def returnedList = _instance.neededToolList(project) returnedList.size() == 1 returnedList.each { if(it.storeId == "T001") { assert it.price == 129.0 } } println "done" } }
咱們看到的第一件事是Groovy爲咱們提供了一種很棒的Mocking代碼機制,它使咱們可以作的比我在Mocking框架中所能作的還要多。在模擬框架中,我一般爲指望返回的數據建立一個新對象。在這裏,我其實是將數據更改成服務應該返回的內容。
切記:我不是在測試服務,因此模擬服務應該返回我指望服務返回的值。
我還發現能夠在一個調用中建立對象並加載數據的功能(與建立Bean和調用每一個setter相對)更容易編寫,讀取和複製爲模板,以建立更多內容。Groovy提供了幾種處理列表的方法,使之成爲快速開發和維護測試的出色語言。
若是您想對單元測試有所不一樣,那麼還有Spock測試框架。它具備更普遍的語言,使其更具行爲驅動的外觀,但仍使用上一示例中的全部Groovy Goodness。
class ToolsSpec extends Specification { def lookupService = [ updateToolList : {List<Tool> toolList -> println "mocked service" toolList.each { tool -> if(tool.storeId == "T001") tool.price = 129.0 } return toolList } ] as LookupService def "Lookup needed tool list"() { given:"Create instance" def _instance = new Tools() def project = new Project() project.tools = [ [name: "table saw", storeId: "T001", promise: [project: project, promised: false] as Promise] as Tool, [name: "pneumatic nail guns", storeId: "T027", promise: [project: project, promised: true] as Promise] as Tool, ] as List<Tool>; _instance.lookupService = lookupService expect:"Tool List" def returnedList = _instance.neededToolList(project) returnedList.size() == 1 returnedList.each { if(it.storeId == "T001") { assert it.price == 129.0 } } } }
請注意,我使用了一種不一樣的語法爲Tool建立測試數據對象。這是標準的Groovy功能,它容許程序員將映射轉換爲具體的類,而且在先前的示例中也可使用。當您習慣閱讀Groovy時,這可能比新的Object語法更容易閱讀。
在這兩個示例中,語法「糖」更緊密的代碼並非惟一的好處。測試失敗的輸出也會有所不一樣,而且會更有幫助
在第一個示例中,測試失敗的輸出爲:
java.lang.AssertionError: expected:<128.0> but was:<129.0> at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:553) at org.junit.Assert.assertEquals(Assert.java:683) at org.projectregistry.services.ToolsTest.testNeededToolList(ToolsTest.java:93) ....
Groovy和Spock測試的輸出以下所示:
Assertion failed: assert it.price == 128.0 | | | | 129.0 false org.projectregistry.model.Tool@5e59238b at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:399) at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:648) at org.projectregistry.services.GroovyToolsTest$_testNeededToolList_closure2.doCall(GroovyToolsTest.groovy:34) ...
Groovy輸出中提供了更多信息,這反過來又使您能夠更快地進行修復。
所以,隨着能夠節省語法和輸出的時間,並但願經過一種新的和不一樣的語言來增長編程樂趣,我但願每一個人均可以嘗試Groovy和/或Spock來克服慣性,這種慣性會阻止程序員進行單元測試。
學習如何簡單。Groovy和Spock都有據可查的文檔,僅經過搜索便可得到許多資源。在各類社交媒體上也有一個很是活躍和樂於助人的社區,我相信很樂意提供幫助。