qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          如何實現Java測試的自定義斷言

           對于測試來說,編寫斷言似乎很簡單:我們只需要對結果和預期進行比較,通常使用斷言方法進行判斷,例如測試框架提供的assertTrue()或者assertEquals()方法。然而,對于更復雜的測試場景,使用這些基礎的斷言驗證結果可能會顯得相當笨拙。
            使用這些基礎斷言的主要問題是,底層細節掩蓋了測試本身,這是我們不希望看到的。在我看來,應該爭取讓這些測試使用業務語言來說話。
            在本篇文章中,我將展示如何使用“匹配器類庫”(matcher library);來實現自定義斷言,從而提高測試代碼的可讀性和可維護性。
            為了方便演示,我們假設有這樣一個任務:讓我們想象一下,我們需要為應用系統的報表模塊開發一個類,輸入兩個日期(開始日期和結束日期),這個類將給出這兩個日期之間所有的每小時間隔。然后使用這些間隔從數據庫查詢所需數據,并以直觀的圖表方式展現給最終用戶。
            標準方法
            我們先采用“標準”的方法來編寫斷言。我們以JUnit為例,當然你也可以使用TestNG。我們將使用像assertTrue()、assertNotNull()或assertSame()這樣的斷言方法。
            下面展示了HourRangeTest類的其中一個測試方法。它非常簡單。首先調用getRanges()方法,得到兩個日期之間所有的每小時范圍。然后驗證返回的范圍是否正確。
          private final static SimpleDateFormat SDF
          = new SimpleDateFormat("yyyy-MM-dd HH:mm");
          @Test
          public void shouldReturnHourlyRanges() throws ParseException {
          // given
          Date dateFrom = SDF.parse("2012-07-23 12:00");
          Date dateTo = SDF.parse("2012-07-23 15:00");
          // when
          final List<range> ranges = HourlyRange.getRanges(dateFrom, dateTo);
          // then
          assertEquals(3, ranges.size());
          assertEquals(SDF.parse("2012-07-23 12:00").getTime(), ranges.get(0).getStart());
          assertEquals(SDF.parse("2012-07-23 13:00").getTime(), ranges.get(0).getEnd());
          assertEquals(SDF.parse("2012-07-23 13:00").getTime(), ranges.get(1).getStart());
          assertEquals(SDF.parse("2012-07-23 14:00").getTime(), ranges.get(1).getEnd());
          assertEquals(SDF.parse("2012-07-23 14:00").getTime(), ranges.get(2).getStart());
          assertEquals(SDF.parse("2012-07-23 15:00").getTime(), ranges.get(2).getEnd());
          }
            毫無疑問這是個有效的測試。然而,它有個嚴重的缺點。在//then后面有大量的重復代碼。顯然,它們是復制和粘貼的代碼,經驗告訴我,它們將不可避免地會產生錯誤。此外,如果我們寫更多類似的測試(我們肯定還要寫更多的測試來驗證HourlyRange類),同樣的斷言聲明將在每一個測試中不斷地重復。
            過多的斷言和每個斷言的復雜性減弱了當前測試的可讀性。大量的底層噪音使我們無法快速準確地了解這些測試的核心場景。我們都知道,閱讀代碼的次數遠大于編寫的次數(我認為這同樣適用于測試代碼),所以我們理所當然地要想辦法提高其可讀性。
            在我們重寫這些測試之前,我還想重點說一下它的另一個缺點,這與錯誤信息有關。例如,如果getRanges()方法返回的其中一個Range與預期不同,我們將得到類似這樣的信息:
            org.junit.ComparisonFailure:
            Expected :1343044800000
            Actual :1343041200000
            這些信息太不清晰,理應得到改善。


           私有方法
            那么,我們究竟能做些什么呢?好吧,最顯而易見的辦法是將斷言抽成一個私有方法:
          private void assertThatRangeExists(List<Range> ranges, int rangeNb,
          String start, String stop) throws ParseException {
          assertEquals(ranges.get(rangeNb).getStart(), SDF.parse(start).getTime());
          assertEquals(ranges.get(rangeNb).getEnd(), SDF.parse(stop).getTime());
          }
          @Test
          public void shouldReturnHourlyRanges() throws ParseException {
          // given
          Date dateFrom = SDF.parse("2012-07-23 12:00");
          Date dateTo = SDF.parse("2012-07-23 15:00");
          // when
          final List<Range> ranges = HourlyRange.getRanges(dateFrom, dateTo);
          // then
          assertEquals(ranges.size(), 3);
          assertThatRangeExists(ranges, 0, "2012-07-23 12:00", "2012-07-23 13:00");
          assertThatRangeExists(ranges, 1, "2012-07-23 13:00", "2012-07-23 14:00");
          assertThatRangeExists(ranges, 2, "2012-07-23 14:00", "2012-07-23 15:00");
          }
            這樣是不是好些?我會說是的。減少了重復代碼的數量,提高了可讀性,這當然是件好事。
            這種方法的另一個優勢是,我們現在可以更容易地改善驗證失敗時的錯誤信息。因為斷言代碼被抽到了一個方法中,所以我們可以改善斷言,很容易地提供更可讀的錯誤信息。
            為了更好地復用這些斷言方法,可以將它們放到測試類的基類中。
            不過,我覺得我們也許能做得更好:使用私有方法也有缺點,隨著測試代碼的增長,很多測試方法都將使用這些私有方法,其缺點將更加明顯:
            斷言方法的命名很難清晰反映其校驗的內容。
            隨著需求的增長,這些方法將會趨向于接收更多的參數,以滿足更復雜檢查的要求。(assertThatRangeExists()現在有4個參數,已經太多了?。?/div>
            有時候,為了在多個測試中復用這些代碼,會在這些方法中引入一些復雜邏輯(通常以布爾標志的形式校驗它們,或在某些特殊的情況下,忽略它們)。
            從長遠來看,所有使用私有斷言方法編寫的測試,意味著在可讀性和可維護性方面將會遇到一些問題。我們來看一下另外一種沒有這些缺點的解決方案。
            匹配器類庫
            在我們繼續之前,我們先來了解一些新工具。正如之前提到的,JUnit或者TestNG提供的斷言缺少足夠的靈活性。在Java世界,至少有兩個開源類庫能夠滿足我們的需求:AssertJ(FEST Fluent Assertions項目的一個分支)和 Hamcrest。我傾向于第一個,但這只是個人喜好。這兩個看起來都非常強大,都能讓你取得相似的效果。我更傾向于AssertJ的主要原因是它基于Fluent接口,而IDE能夠完美支持該接口。
            集成AssertJ和JUnit或者TestNG非常簡單。你只要增加所需的import,停止使用測試框架提供的默認斷言方法,改用AssertJ提供的方法就可以了。
            AssertJ提供了一些現成的非常有用的斷言。它們都使用相同的“模式”:先調用assertThat()方法,這是Assertions類的一個靜態方法。該方法接收被測試對象作為參數,為更多的驗證做好準備。之后是真正的斷言方法,每一個都用于校驗被測對象的各種屬性。我們來看一些例子:
            assertThat(myDouble).isLessThanOrEqualTo(2.0d);
            assertThat(myListOfStrings).contains("a");
            assertThat("some text")
            .isNotEmpty()
            .startsWith("some")
            .hasLength(9);
            從這能看出,AssertJ提供了比JUnit和TestNG豐富得多的斷言集合。就像最后一個assertThat("some text")例子顯示的,你甚至可以將它們串在一起。還有一個非常方便的事情是,你的IDE能夠根據被測對象的類型,自動為你提示可用的方法。舉例來說,對于一個double值,當你輸入“assertThat(myDouble).”,然后按下CTRL + SPACE(或者其它IDE提供的快捷鍵),IDE將為你顯示可用的方法列表,例如isEqualTo(expectedDouble)、isNegative()或isGreaterThan(otherDouble),所有這些都可用于double值的校驗。這的確是一個很酷的功能。
          自定義斷言
            擁有AssertJ或者Hamcrest提供的更強大的斷言集合的確很好,但對于HourRange類來說,這并不是我們真正想要的。匹配器類庫的另一個功能是允許你編寫自己的斷言。這些自定義斷言的行為將與AssertJ的默認斷言一樣,也就是說,你能夠把它們串在一起。這正是我們接下來要做的。
            接下來我們將看到一個自定義斷言的示例實現,但現在讓我們先看看最終效果。這次我們將使用(我們自己的)RangeAssert類的assertThat()方法。
          @Test
          public void shouldReturnHourlyRanges() throws ParseException {
          // given
          Date dateFrom = SDF.parse("2012-07-23 12:00");
          Date dateTo = SDF.parse("2012-07-23 15:00");
          // when
          List<Range> ranges = HourlyRange.getRanges(dateFrom, dateTo);
          // then
          RangeAssert.assertThat(ranges)
          .hasSize(3)
          .isSortedAscending()
          .hasRange("2012-07-23 12:00", "2012-07-23 13:00")
          .hasRange("2012-07-23 13:00", "2012-07-23 14:00")
          .hasRange("2012-07-23 14:00", "2012-07-23 15:00");
          }
            即便是上面這么小的一個例子,我們也能看出自定義斷言的一些優勢。首先要注意的是//then后面的代碼確實變少了,可讀性也更好了。
            將自定義斷言應用于更大的代碼庫時,將顯現出其它優勢。當我們繼續使用自定義斷言時,我們將注意到:
            可以很容易地復用它們。我們不強迫使用所有斷言,但對特定測試用例,我們可以只選擇那些重要的斷言。
            特定領域語言屬于我們,也就是說,對于特定測試場景,我們可以根據自己的喜好改變它(例如,傳入Date對象,而不是字符串)。更重要的是這樣的改變不會影響到其它測試。
            高可讀性。毫無疑問,因為斷言包括了很多小斷言方法,每一個都只關注校驗的很小的某個方面,因此可以為校驗方法取一個恰當的名字。
            與私有斷言方法相比,自定義斷言的唯一不足是工作量要大一些。我們來看一下自定義斷言的代碼,它是否真的是一個很難的任務。
            要創建自定義斷言,我們需要繼承AssertJ的AbstractAssert類或者其子類。如下所示,我們的RangeAssert繼承自AssertJ的ListAssert類。這很正常,因為我們的自定義斷言將校驗一個Range列表(List<Range>)。
            每一個使用AssertJ的自定義斷言都會包含創建斷言對象、注入被測對象的代碼,然后可以使用更多的方法對其進行操作。如下面的代碼所示,構造方法和靜態assertThat()方法的參數都是List<Range>。
            public class RangeAssert extends ListAssert<Range> {
            protected RangeAssert(List<Range> ranges) {
            super(ranges);
            }
            public static RangeAssert assertThat(List<Range> ranges) {
            return new RangeAssert(ranges);
            }
            現在我們看看RangeAssert類的其余內容。hasRange()和isSortedAscending()方法(顯示在下一個代碼列表中)是自定義斷言方法的典型例子。它們具有以下共同點:
            它們都先調用isNotNull()方法,檢查被測對象是否為null。確保這個校驗不會失敗并拋出NullPointerException異常消息。(這一步不是必須的,但建議有這一步)
            它們都返回“this”(也就是自定義斷言類的對象,對應例子中RangeAssert類的對象)。這使得所有方法可以串在一起。
            它們都使用AssertJ Assertions類(屬于AssertJ框架)提供的斷言方法執行校驗。
            它們都使用“真實”的對象(由父類ListAssert提供),確保Range列表(List<Range>)被校驗。
          private final static SimpleDateFormat SDF
          = new SimpleDateFormat("yyyy-MM-dd HH:mm");
          public RangeAssert isSortedAscending() {
          isNotNull();
          long start = 0;
          for (int i = 0; i < actual.size(); i++) {
          Assertions.assertThat(start)
          .isLessThan(actual.get(i).getStart());
          start = actual.get(i).getStart();
          }
          return this;
          }
          public RangeAssert hasRange(String from, String to) throws ParseException {
          isNotNull();
          Long dateFrom = SDF.parse(from).getTime();
          Long dateTo = SDF.parse(to).getTime();
          boolean found = false;
          for (Range range : actual) {
          if (range.getStart() == dateFrom && range.getEnd() == dateTo) {
          found = true;
          }
          }
          Assertions
          .assertThat(found)
          .isTrue();
          return this;
          }
          }
            那么錯誤信息呢?AssertJ讓我們可以很容易地添加錯誤信息。對于簡單的場景,例如值的比較,通常使用as()方法就足夠了,示例如下:
            Assertions
            .assertThat(actual.size())
            .as("number of ranges")
            .isEqualTo(expectedSize);
            正如你所見到的,as()只是AssertJ框架提供的另一個方法。當測試失敗時,它打印下面的信息,我們立即就能知道哪兒錯了:
            org.junit.ComparisonFailure: [number of ranges]
            Expected :4
            Actual :3
            有時候只知道被測對象的名字是不夠的,我們需要更多信息以了解到底發生了什么。以hasRange()方法為例,當測試失敗時,如果能夠打印所有range就更好了。我們可以通過overridingErrorMessage()方法來實現這種效果:
            public RangeAssert hasRange(String from, String to) throws ParseException {
            ...
            String errMsg = String.format("ranges\n%s\ndo not contain %s-%s",
            actual ,from, to);
            ...
            Assertions.assertThat(found)
            .overridingErrorMessage(errMsg)
            .isTrue();
            ...
            }
            現在,當測試失敗時,我們能夠得到非常詳細的信息。它的內容取決于Range類的toString()方法。例如,它看起來可能是這樣的:
            HourlyRange{Mon Jul 23 12:00:00 CEST 2012 to Mon Jul 23 13:00:00 CEST 2012},
            HourlyRange{Mon Jul 23 13:00:00 CEST 2012 to Mon Jul 23 14:00:00 CEST 2012},
            HourlyRange{Mon Jul 23 14:00:00 CEST 2012 to Mon Jul 23 15:00:00 CEST 2012}]
            do not contain 2012-07-23 16:00-2012-07-23 14:00
            總結
            在本文中,我們討論了很多編寫斷言的方法。我們從“傳統”的方式開始,也就是基于測試框架提供的斷言方法。對于很多場景,這已經非常好了。但是正如我們所看到的,它在表達測試意圖時,有時候缺少了一些靈活性。之后,我們通過引入私有斷言方法,取得了一點改善,但仍然不是理想的解決方案。最后,我們嘗試使用AssertJ編寫自定義斷言,我們的測試代碼取得了非常好的可讀性和可維護性。
            如果要我提供一些關于斷言的建議,我將會建議以下內容:如果你停止使用測試框架(例如JUnit或TestNG)提供的斷言,改為使用匹配器類庫(例如AssertJ或者Hamcrest),你的測試代碼將得到極大的改善。你將可以使用大量可讀性很強的斷言,減少測試代碼中//then之后的復雜聲明。
            盡管編寫自定義斷言的成本非常低,但也沒有必要因為你會寫就一定要使用它們。當你的測試代碼的可讀性并且/或者可維護性變差時使用它們。根據我的經驗,我會鼓勵你在以下場景中使用自定義斷言:
            當你發現使用匹配器類庫提供的斷言無法清晰表達測試意圖時;
            作為私有斷言方法的替代方案。
            我的經驗告訴我,單元測試幾乎不需要自定義斷言。而在集成測試和端到端測試(功能測試)中,我敢說你肯定會發現它們是不可替代的。它們能讓你的測試用領域語言說話(而不是實現語言),它們還封裝了技術細節,使測試更易于更新。
            關于作者
            Tomek Kaczanowski是CodeWise公司(克拉科夫,波蘭)的一名Java開發人員。他專注于代碼質量、測試和自動化。他是TDD的狂熱者、開源的倡導者和敏捷的崇拜者。具有強烈的分享知識傾向。書的作者、博客和會議發言人。Twitter: @tkaczanowski


          posted on 2014-07-03 18:39 順其自然EVO 閱讀(504) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          <2014年7月>
          293012345
          6789101112
          13141516171819
          20212223242526
          272829303112
          3456789

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 吉安市| 延安市| 关岭| 临清市| 广汉市| 临江市| 屏东县| 肥城市| 民勤县| 武川县| 来安县| 扬中市| 宁化县| 汪清县| 东莞市| 眉山市| 衢州市| 南和县| 林西县| 德令哈市| 济宁市| 新昌县| 亚东县| 尤溪县| 彩票| 滨州市| 扶余县| 屏山县| 敦化市| 吉隆县| 萍乡市| 宜良县| 通化县| 平阴县| 囊谦县| 大化| 万安县| 上高县| 北流市| 澄迈县| 嘉禾县|