人生苦短?試試Groovy進行單元測試

  • 2019 年 11 月 21 日
  • 筆記

如果您今天正在編程,那麼您很可能聽說過單元測試或測試驅動的開發過程。我還沒有遇到一個既沒有聽說過又沒有聽說過單元測試並不重要的程式設計師。在隨意的討論中,大多數程式設計師似乎認為單元測試非常重要。

但是,當我開始使用程式碼並問「單元測試在哪裡?」時,我得到了一個完全不同的故事。我最近在網上問我的程式設計師朋友為什麼不這樣做,以及為什麼其他程式設計師不這樣做呢?不要編寫單元測試。當我問程式設計師或IT經理同樣的問題時,我經常聽到的第一答案是:「我沒有時間」或類似的問題。通常會出現這樣的論點,即使用單元測試編寫應用程式要比不使用單元測試編寫時間長20%,並且「我們受到時間限制」。

我的建議–當我們嘗試解決時間不足的問題時,也許我們可以在娛樂性上做出一些貢獻。

在實踐中

我正在為一個應用程式設計原型,該應用程式將允許用戶輸入有關房屋裝修項目的資訊,然後與朋友共享該項目的材料和工具資訊。然後,朋友可以承諾貸款或購買項目中所需的一些材料或工具。基本上是用於家庭裝修項目的「登記處」。

測試將在採用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帶來了許多語法上的捷徑,這些捷徑有助於加快編寫程式碼(包括測試)的速度。讓我們看一下在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都有據可查的文檔,僅通過搜索即可獲得許多資源。在各種社交媒體上也有一個非常活躍和樂於助人的社區,我相信很樂意提供幫助。