ABAP單元測試最佳實踐


 

  本文包含了我在開發項目中經歷過的實用的ABAP單元測試指導方針。我把它們安排成為問答的風格,歡迎任何人添加更多的Q&A's,以完成這個列表。

 

  • 在我的項目中,只使用傳統的ABAP report。所以很不幸我不能使用ABAP單元測試了,是嗎?
    有個好消息:無論你正在使用哪一種ABAP代碼對象進行開發,都可以通過添加單元測試使得它更加穩定和更易於擴展。對於reports,模塊池(module pools)和函數組(function groups),可以通過添加手寫本地類的方式添加單元測試。假設一個簡單的情形,在一個report中你想要測試子程序xyz的最直接調用,下面的代碼骨架就可以做到,這段代碼可以定義為代碼模板,以便於插入到report。
    class lcl_test definition for testing  "#AU Duration Short
      inheriting from cl_aunit_assert. "#AU Risk_Level Harmless
     private section.
        methods test_xyz_simple_call for testing.
    endclass.
     
    class lcl_test implementation.
      method test_xyz_simple_call.
    * Setup parameters for the call...
    * Perform the call
     perform xyz using ...
    * Check returned values
        assert_equals( act = ... exp = ... ).
      endmethod.
    endclass.

    當然,使用ABAP面向對象有很多好處,比如,會有ABAP類的單元測試模板的自動生成功能。同樣地,生產代碼和測試代碼的分界會更清晰。測試類聲生成在一個用於單元測試的“包含(incluede)”部分,會同其它內容隔離。如果出於某些原因不需要使用這些類,你依然擁有單元測試支持。

 

  • 不幸的是,我的客戶的開發系統中的主數據質量太差了,以至於我用不了單元測試。
    又有好的消息:盡管客戶的開發系統有着糟糕的數據質量,你還是可以做單元測試!單元測試的最大優勢之一,就是可以獨立地測試單一的代碼單元——測試不依賴任何數據庫條目,不依賴其它中途調用的函數模塊。如果測試在500客戶端沒問題,那它在000客戶端同樣可以運行得很好。

  

  • 不是真的想讓我為所有開發過的代碼對象寫單元測試吧?

    不,並沒有。為某些代碼寫單元測試會導致時間的浪費。它們是:

    1. 自動生成的代碼,比如視圖維護函數組,靜態系統信息報表,BSP擴展基礎類,以及其它相似的東西。
    2. 大多數數據庫查詢。在多數情況下,數據庫查詢不應該在單元測試內執行(關於該點請看下文)。有一些例外,比如DAOs(Data Access Object)。這些是單個數據庫的專家(?)。在某些特殊情況下,為了測試功能性而創建測試條目(並且在teardown階段移除)是行得通的。
    3. 連接dynpro和abap代碼的代碼。有一些需要重定向dynpro的膠水代碼,就像PAI(Process After Input)中一個特定的對ABAP代碼塊的鏈式請求所起的作用那樣。通常是不值得花費努力為這樣的膠水代碼進行單元測試的。

 

  • 某個類做的事情是不重要的,不值得為它做測試。
    也許你是對的,但通常,你錯了。只是你認為你的代碼不重要,因為你只是完成了它的編寫。經驗表明,一年后,先前不重要的代碼,看起來再也不會對你不重要了。你的同事也同樣不會認為它不重要。如果你只是實現了一個適配器類,將一個數據格式映射為另一個,接着調用一個API,也許你是對的:對這樣的類進行單元測試也許是過度工程。但是隨着源代碼體積的上升,看起來不重要的代碼也許包含某些bug,這種bug只有在被調用的時候才會顯現。為什么不實現一個可以自動檢查期望結果的調用呢?這樣做可以保證該類在任何時候都工作正常。

 

  • 單元測試需要同測試驅動開發(TDD)共同進行嗎?
    基本上,TDD是一種意為“首先實現測試,之后添加可以使得測試通過的生產代碼”的編程實踐。這是一個“乒乓球”過程,你將總是在新的測試代碼和新的生產代碼間轉向。你不需要實踐TDD,但是如果你習慣了它,會在很大程度上幫助你避免bug,並因此變得更有效率。即使不使用測試驅動,你依然會受益於單元測試:可以向已存在的代碼對象添加事后比較檢驗(post-hoc test)。 

 

  • 單元的外部測試怎么樣?使用單獨的測試對象。
    可以在一個類的屬性標簽中指定其作為單元測試類。但是這應當用於通過繼承來提取幾個相似的本地單元測試類的測試代碼的情況,而不應用在單個單元的測試上面。通常,在外部測試一個單元是不建議的做法,因為這使得單元測試在工作台菜單路徑中的“模塊測試”里成為不可用的狀態。如果你的類被某人改動了,他也許沒有意識到代碼應當通過外部程序的測試。因此更好的做法是把它包含在生產代碼所在的同一個對象當中。

 

  • 如果我不測試所有的代碼,測試覆蓋中會出現斷層!
    雖然單元測試是個很有用的工具,但並不能回應所有的需求。我在上面已經提到,這種斷層不建議使用單元測試處理,而是應該使用被稱為集成測試的其它技術進行覆蓋,比如eCATT, QTP或者其它。

 

  • 我該怎樣設計單元測試?

    要點在於:應該將它們設計的盡可能簡單。單元測試同樣起着單元功能文檔的作用。同樣地,如果執行修改后,單元測試失敗,會很容易從代碼中看出哪個功能失敗了。嘗試避免測試方法中的多余代碼。將重復代碼包裝到方法中甚至宏之中,以保證在測試下功能的實質更加可讀。直率地命名變量、方法、類和宏,使得代碼在測試時盡可能的具有表達力。單元的每個特性,都需要可以按照以下三步測試:

    1. 建立測試數據——填充接口參數的內表或屬性,以及/或者
    2. 調用測試方法——通常正好是對公共方法的調用。
    3. 檢測方法輸出的異常。
      這三步應當被包含在一個測試方法中。在每個測試方法附近,通常是測試對象構建的地方,會有一個建立步驟(對於類的每一個方法都是相同的),如果有需要的話,會提供樁。同樣的,每個測試方法的調用后伴隨着一個teardown調用。

 

  • 我怎樣識別自己的方法其實在被單元測試調用,而不是真的用戶?我想在這種情形下做點不同的事情。
    別這樣!不要將生產代碼和測試代碼混在一起,如果想要為了測試而消除生產代碼中的一部分,應當使用樁和依賴注入來代替。但是,在生產代碼使用一個“測試模式”的標識,會破壞單元測試的概念,並且導致的代碼變得更糟糕。

 

  • 我要怎樣組織自己的代碼?
    沒有用於組織單元測試的通行方案。有時讓每個方法有一個單元測試類、每個輸入數據的等價類有一個測試方法是好的做法。但這不是一般的規則。一般來說,測試方法在正交時會變得有用:理想情況下,每個方法測試測試一個不依賴其它存在的單一功能。不要讓測試方法負擔過多的斷言。

 

  • 如何測試一個將數據庫查詢和它自己的業務邏輯混合到一起並且調用了其它函數模塊的程序(方法/函數模塊)?
    The redefined helper classes like lcl_api_test and lcl_db_test is what the test people call stubs.首先讓代碼成為可測試的,例如使用樁:將數據庫查詢和函數模塊調用包裝到本地幫助類中(數據庫方面我使用LCL_DB,調用其它代碼單元方面我使用LCL_API ),提取這些代碼到自己的方法里。為這些方法使用具有表達性的名字,使用適配器模式為它們設計一個良好的接口。之后你的LCL_API和LCL_DB將只包含外部函數模塊調用和數據庫操作(select, insert, update, enqueue, ...),也許會有幾行映射代碼,用於將你設計的好的接口映射到你調用的模塊的傳統接口。
    在你的對象中應有像go_api和godb這樣的全局的幫助類實例可用。在測試方法中重定義其方法,控制他們的行為。像lcl_api_test和lcl_db_test這樣的重定義過的幫助類就是測試人員所說的的“樁”。

 

  • 聽起來是復雜的。
    你說得對,它不是直接的。為了保持測試代碼簡單可理解,你應當嘗試在任何時候盡可能避免樁的使用。可以通過在業務邏輯、API調用和數據庫操作之間提供更好的分隔,來避免樁。例如,不在相同的方法里面查詢數據、對數據執行檢查。可以首先查詢數據,接着將數據條目作為導入參數在自己的方法里進行檢查。通過這種方法讓代碼變得可測試,通常——作為副作用——會提高其可讀性。

 

  • 我應該測試受保護方法或者私有方法么?
    通常不用。通常,你會關注一個類的公共界面。私有屬性或方法也許會在重構期間消失,或者被其它組件代替。如果它對公共方法調用沒有任何影響,就算刪除它,也許都是安全的;如果它對公共方法調用有影響,那就測試公共方法——保持未來重構的自由。如果測試私有方法,接着,想要改變這些組件的時候,就不得不改變它們的單元測試,這導致代碼的可變性很差。

 

  • 好的——但是,我在某種特別的狀況下(blabla...)真的很需要測試私有方法和受保護的方法。我要怎樣提供這個?
    因為,像任何其它類一樣,本地類是獨立於它們的包含工作台類的,你需要聲明本地測試類為包含類的友元。如果zcl_testee是包含類,lcl_test是單元測試類,需要在本地測試類中添加如下代碼:
    class lcl_test definition deferred.
    class zcl_testee definition local friends lcl_test.
    ...
    class lcl_test definition for testing ...
    ...

 

  • 我的單元測試包含語法錯誤,但是它對生產類沒影響,因為單元測試只在開發系統中進行。對嗎?
    (warning) 不是的。單元測試不可以在生產系統中執行,但是類中的單元部分里面的語法錯誤會破壞完整的類,導致訪問類的任何屬性或方法時會出現SYNTAX_ERROR的short dump。

 

  • 我的測試對象是一個單例。為了避免副作用,我想至少對每個方法的測試得創建一個新的實例。
    如果你的單例包含全局數據,它們也許會被測試改變,在測試調用之間生成丑陋的依賴。你可以在測試期間通過屬性“create public”創建一個對象的子類,按照如下方法進行。
    如果你只是需要類行為的這種改變,你甚至不需要子類的“class...implementation”部分。
    class lcl_testee definition inheriting from zcl_someclass create public.
    endclass.
    ...
    class lcl_test implementation.
      method setup.
        create object go_testee type lcl_testee.
      endmethod.
    endclass.

    記着,無論如何,問題不會由單元測試而是全局數據引起。單元測試只是發現問題,而不是導致問題。因此最佳的解決方式是排除類中的全局數據。

 

  • 我要怎樣做能讓我的測試代碼變得更加可讀?
    • 無論在任何時候,盡可能地使用隱式的“函數”表示法進行方法調用,特別是像assert( ), assert_initial(), assert_subrc()等等這種調用。
    • 如果你不需要測試類的繼承層次(為什么需要?),你也許會讓測試類繼承自cl_aunit_assert。可以像這樣寫:

      assert_subrc( sy-subrc ).
      而不是

      call method cl_aunit_assert=>assert_subrc
        exporting
          act = sy-subrc.
      • 如果調用是復雜的(比如含有很多參數),使用宏填充內表和調用測試方法。省去調用自身的重復代碼,也省去了用於填充內表的本地變量比如工作區。我們使用一種宏包含子程序池的結合來填充內表,減少了用於工作區的輔助本地變量的需要。
        如果你需要一個例子:這里是一個用於解析器的測試方法,可以將指定的包裝規則轉換為自由文本並將其放入內表中,內表中包含以預定義格式存在的相關信息。建立自由文本、調用解析器方法、檢查結果內表的特定組件,這三種行為,在約20個不同方法中是重復的,只有自由文本的內容和修改的內表中的預期結果會改變。
        宏_assert_n_fields_in_row檢查指定內表的指定行的指定的組件含有指定的值!
    • method test_2_lief_2_pal.
       
      * Test assignment of deliveries to handling units
       
          _set_code:
            `1. Palette  (                 `,
            `  1. Lieferung, 1. Pos, 50%   `,
            `  )                           `,
            `2. Palette  (                 `,
            `  2. Lieferung, 2. Pos, Rest  `,
            ` )                            `.
       
          _call_parser.
       
          _assert_rows 'Pack data' gt_packdata_template 2.
       
          _assert_n_fields_in_row 'Pack data' gt_packdata_template 'exidv;vepos;vbeln;posnr;vemng;vemeh;unvel' :
             1 'E1;1;1;1;50;!%;',
             2 'E2;1;2;2;REST;;'.
       
        endmethod.
      用這種方式提取重復的代碼,減少上面提到過的用於“建立——測試調用——校驗”三個步驟的方法,以明確受測試的功能。
    • 讀一些好書,比如Martin Fowler的《重構》,或者Robert C. Martin的《代碼整潔之道》以獲取更多關於代碼如何變得更加可讀的思想。

 

  • 使用宏對調試不利嗎?
     視情況而定,如果只是使用宏來“去掉噪音”,比如,用來提取總是一樣並且頻繁使用到的代碼序列,那么在調試器里面使用F6跳過它的執行就不是問題。如果你有一個隱藏了像上面例子中的_call_parser一樣的方法調用的宏,你可以使用F5進入該方法,即使調用隱藏在宏里面。此外,在這種情形下,你只是失去了代碼中無趣的部分。

 

  • 在一個作業中周期性地運行單元測試是有意義的嗎?
    通常,單元測試和新代碼的開發相關聯。與集成測試相反,在夜間作業運行它們並不讓人意外,因為結果只在代碼改變的時候改變,因此代碼的最后修改者應該知道結果——如果他測試了他的單元!如果你的團隊中有不使用單元測試的開發者,或者代碼的最后一個修改者僅僅是忘記了運行單元測試,有個作業來通知失敗,會很不錯(比如通過發送郵件給TADIR的擁有者)。你可以使用代碼檢查器(code inspector)運行單元測試。別忘記在單元測試類定義中的有關風險等級的偽代碼注釋和期間,因為,否則的話,代碼檢查其也許會不執行測試:
    class lcl_test definition for testing  "#AU Duration Short
      inheriting from cl_aunit_assert. "#AU Risk_Level Harmless
     ...



  • 在傳輸請求將要發布的時候檢查單元測試是可行的嗎?
    可以,而且我認為它很有用。最簡單的達成方式是打開傳輸發布的代碼檢查器檢查,並在檢查變量中選擇“單元測試”。
    在我們的實踐中,我選擇了一個更 復雜的方式,使用傳輸組織器的BAdI和一個函數模塊調用單元測試。雖然這個功能沒有得到SAP的保障(短文本中包含危險修訂“for SAP only”),它還是看起來工作的相當好。我們從兩年前開始使用它,到現在也沒出問題。方法cl_aunit_prog_info=> contain_programs_testcode( )也許可以用於找出特定的程序(根據指定主程序的報表源的名字)是否包含單元測試。如果僅僅是程序、類或者函數模塊的一部分改變了,你也許不得不找出LIMU的父對象。為實現這點,可以使用函數模塊TR_CHECK_TYPE。

 

本文鏈接:http://www.cnblogs.com/hhelibeb/p/6038202.html

原文鏈接:ABAP Unit Best Practices 

 

2018.04.22更新:現有一個Open SAP的Writing Testable Code for ABAP 視頻教程,推薦觀看

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM