<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          工作多年后我更體會(huì)到單元測試的重要性

          共 32279字,需瀏覽 65分鐘

           ·

          2021-03-14 19:26

          對于有經(jīng)驗(yàn)的開發(fā)寫單元測試是非常有必要的,并且對自己的代碼質(zhì)量以及編碼能力也是有提高的。單元測試可以幫助減少bug泄露,通過運(yùn)行單元測試可以直接測試各個(gè)功能的正確性,bug可以提前發(fā)現(xiàn)并解決,由于可以跟斷點(diǎn),所以能夠比較快的定位問題,比泄露到生產(chǎn)環(huán)境再定位要代價(jià)小很多。同時(shí)充足的UT是保證重構(gòu)正確性的有效手段,有了足夠的UT防護(hù),才能放開手腳大膽重構(gòu)已有代碼,工 作多年后更了解了UT,了解了UT的重要性。

          單元測試

          在敏捷的開發(fā)理念中,覆蓋全面的自動(dòng)化測試是添加新特性和重構(gòu)的必要前提。單元測試在軟件開發(fā)過程中的重要性不言而喻,特別是在測試驅(qū)動(dòng)開發(fā)的開發(fā)模式越來越流行的前提下,單元測試更成為了軟件開發(fā)過程中不可或缺的部分。同時(shí)單元測試也是提高軟件質(zhì)量,花費(fèi)成本比較低的重要方法。

          1.單元測試的時(shí)機(jī)和測試點(diǎn)

          1.1單元測試的時(shí)機(jī)

          1. 在業(yè)務(wù)代碼前編寫單元測試采用測試驅(qū)動(dòng)開發(fā),這是我們經(jīng)常使用和推薦的。
          2. 在業(yè)務(wù)代碼過程中進(jìn)行單元測試,對重要的業(yè)務(wù)邏輯和復(fù)雜的業(yè)務(wù)邏輯進(jìn)行添加測試。
          3. 在業(yè)務(wù)邏輯之后再編寫測試是我們不建議的,除非對遺留代碼的修改,需要先進(jìn)行測試用例的添加,保證我們修改和重構(gòu)后的代碼不會(huì)破壞之前的業(yè)務(wù)邏輯。

          1.2單元測試的測試點(diǎn)

          1. 在邏輯復(fù)雜的代碼中添加測試。
          2. 在容易出錯(cuò)的地方添加測試。
          3. 不易理解的代碼中添加測試,在以后看到測試就可以非常清楚代碼要實(shí)現(xiàn)的邏輯。
          4. 在考慮后期需求變更相對較大的代碼中添加測試,這樣后期需求更變修改代碼之后就不用太擔(dān)心寫的代碼對不對以及是否破壞了已有代碼邏輯。
          5. 外部接口處添加解耦代碼、同時(shí)增加單元測試。

          2.代碼不可測試性的根源

          1. 代碼中調(diào)用到了底層平臺(tái)的接口或只有系統(tǒng)運(yùn)行后才能獲得的資源(數(shù)據(jù)庫連接、發(fā)送郵件,網(wǎng)絡(luò)通訊,遠(yuǎn)程服務(wù), 文件系統(tǒng)等)但業(yè)務(wù)代碼與這些資源未解耦。這樣在測試代碼需要?jiǎng)?chuàng)建這個(gè)類的時(shí)候會(huì)去初始化這些資源時(shí)導(dǎo)致無法測試。
          2. 在方法內(nèi)部new一個(gè)與本次測試無關(guān)的對象。
          3. 代碼依賴層次很深,邏輯復(fù)雜,一次方法的往往要調(diào)用N次底層的接口,或者類的方法非常多。這樣的代碼我們需要對類進(jìn)行重構(gòu),盡量保證類的單一職責(zé):這個(gè)類在系統(tǒng)中的意圖應(yīng)當(dāng)是單一的,且修改它的原因應(yīng)該只有一個(gè)。
          4. 使用單例類和靜態(tài)方法,并且單例類和靜態(tài)方法使用到了我們底層的接口或者其他接口。

          3.測試工具使用和測試方法介紹

          在做單元測試的時(shí)候,我們會(huì)發(fā)現(xiàn)我們要測試的方法會(huì)引用很多外部依賴的對象,如調(diào)用平臺(tái)接口、連接數(shù)據(jù)庫、網(wǎng)絡(luò)通訊、遠(yuǎn)程服務(wù)、FTP、文件系統(tǒng)等等。 而我們沒法控制這些外部依賴的對象,為了解決這個(gè)問題,我們就需要用到Mock工具來模擬這些外部依賴的對象,來完成單元測試。 現(xiàn)在比較流行的Mock工具有JMock、EasyMock、Mockito、PowerMock。我們使用的是Mockito和PowerMock。PowerMock彌補(bǔ)了其他3個(gè)Mock工具不能mock靜態(tài)、final 、私有方法的缺點(diǎn)。 在下面的情況下我們可以使用Mock對象來完成單元測試。

          1. 實(shí)對象具有不可確定的行為,會(huì)產(chǎn)生不可預(yù)測的結(jié)果。 如:數(shù)據(jù)庫查詢可以查出一條記錄、多條記錄、或者返回?cái)?shù)據(jù)庫異常等結(jié)果。
          2. 真實(shí)對象很難被創(chuàng)建。如:平臺(tái)代碼,或者Web、JBoss容器等。
          3. 真實(shí)對象的某些行為很難觸發(fā)。 如:代碼中需要處理的網(wǎng)絡(luò)異常、數(shù)據(jù)庫異常、消息發(fā)送異常等。
          4. 真實(shí)情況令程序運(yùn)行很慢。 在敏捷的實(shí)踐中我們完成了CI,在開發(fā)提交代碼前需要執(zhí)行整個(gè)項(xiàng)目的單元測試用例,只有測試通過才可以提交代碼。這就要求我們每個(gè)單元測試用例需要盡可能的短,整個(gè)項(xiàng)目的測試時(shí)間才會(huì)短。當(dāng)有的測試用例需要測試大數(shù)據(jù)量情況下系統(tǒng)的預(yù)期時(shí),就需要使用Mock對象。 如我們代碼中需要判斷只有當(dāng)系統(tǒng)的緩存隊(duì)列大于40000時(shí),我們開始考慮丟棄非關(guān)鍵的消息,當(dāng)超過48000時(shí),需要只處理最重要的消息,當(dāng)超過50000時(shí)需要丟棄全部消息。此時(shí)就需要對此緩存隊(duì)列進(jìn)行Mock,根據(jù)調(diào)用返回不同的數(shù)據(jù)量給測試。
          5. 測試需要知道真實(shí)對象是如何被調(diào)用的。如:測試用例需要驗(yàn)證是否發(fā)送了JMS,此時(shí)就可以通過Mock對象是否被調(diào)用來測試。
          6. 真實(shí)對象實(shí)際不存在時(shí)。 如:當(dāng)我們與其他模塊交互時(shí),或者與新的接口打交道時(shí),更有就是對方的代碼還沒有開發(fā)完畢時(shí),我們可以通過Mock來模擬接口的行為,實(shí)現(xiàn)代碼邏輯的驗(yàn)證和測試。

          3.1 Mocktio簡單使用說明

          mock可以模擬各種各樣的對象,從而代替真正的對象做出希望的響應(yīng)。

          1、模擬對象的創(chuàng)建

          List cache = mock(ArrayList.class);
          System.out.println(cache.get(0));
          //-> null 由于沒有對mock對象給預(yù)期,所以返回都是null

          2、模擬對象方法調(diào)用的返回值

          List cache = mock(ArrayList.class);
          when(cache.get(0)).thenReturn("hello");
          System.out.println(cache.get(0));
          //-> hello

          3、模擬對象方法多次調(diào)用和多次返回值

          List cache = mock(ArrayList.class);
          when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2");
          System.out.println(cache.get(0));
          System.out.println(cache.get(0));
          System.out.println(cache.get(0));
          System.out.println(cache.get(0));
          //-> 0,1,2,2 如果實(shí)際調(diào)用的次數(shù)超過了預(yù)期的次數(shù),則會(huì)一直返回最后一次的預(yù)期值。

          4、模擬對象方法調(diào)用拋出異常

          List cache = mock(ArrayList.class);
          when(cache.get(0)).thenReturn(new Exception("Exception"));
          System.out.println(cache.get(0));

          5、模擬對象方法在沒有返回值時(shí)也可以拋異常

          List cache = mock(ArrayList.class);
          doThrow(new Exception("Exception")).when(cache).clear();

          6、模擬方法調(diào)用時(shí)的參數(shù)匹配

          AnyInt的使用,匹配任何int參數(shù)
          List cache = mock(ArrayList.class);
          when(cache.get(anyInt())).thenReturn("0");
          System.out.println(cache.get(0));
          System.out.println(cache.get(2));
          //-> 0,0

          7、模擬方法是否被調(diào)用和調(diào)用的次數(shù),預(yù)期調(diào)用了一次

          List cache = mock(ArrayList.class);
          cache.add("steven");
          verify(cache).add("steven");

          預(yù)期調(diào)用了兩次入緩存,沒有調(diào)用清除緩存的方法

          List cache = mock(ArrayList.class);
          cache.add("steven");
          cache.add("steven");
          verify(cache,times(2)).add("steven");
          verify(cache,never()).clear();

          還可以通過atLeast(int i)和atMost(int i)來替代times(int i)來驗(yàn)證被調(diào)用的次數(shù)最小值和最大值。【注意】Mock對象默認(rèn)情況下,對于所有有返回值且沒有預(yù)期過的方法,Mocktio會(huì)返回相應(yīng)的默認(rèn)值。

          對于內(nèi)置類型會(huì)返回默認(rèn)值,如int會(huì)返回0,布爾值返回false。對于其他type會(huì)返回null。mock對象會(huì)覆蓋整個(gè)被mock的對象,因此沒有預(yù)期的方法只能返回默認(rèn)值。這個(gè)在初次使用Mock時(shí)需要注意,經(jīng)常會(huì)發(fā)現(xiàn)測試結(jié)果不對,最后才發(fā)現(xiàn)自己未給相應(yīng)的預(yù)期。

          3.2 PowerMock簡單使用說明

          PowerMock使用一個(gè)自定義類加載器和字節(jié)碼操作來模擬靜態(tài)方法,構(gòu)造函數(shù),final類和方法,私有方法,去除靜態(tài)初始化器等等。 PowerMock使用簡單,在類名前添加注解,在預(yù)期前調(diào)用PowerMock的mock靜態(tài)類方法,其他的預(yù)期方法和Mockito類似。

          @PrepareForTest(System.class)
          @RunWith(PowerMockRunner.class)
          public class Test 
          {
          @org.junit.Test
          public void should_get_filed() 
          {
           System.out.println(System.getProperty("myName"));
           PowerMockito.mockStatic(System.class);
           PowerMockito.when(System.getProperty("myName")).thenReturn("steven");
           System.out.println(System.getProperty("myName"));
           //->null steven
           }
          }

          3.3 Fake對象的使用

          測試中需要模擬對象,除了常用的mock對象外,我們還會(huì)經(jīng)常用到Fake對象。Mock對象是預(yù)先計(jì)劃好的對象,帶有各種期待,他們組成了一個(gè)關(guān)于他們期待接受的調(diào)用的詳細(xì)說明。

          而Fake對象是有實(shí)際可工作的實(shí)現(xiàn),但是通常有一些缺點(diǎn)導(dǎo)致不適合用于產(chǎn)品,我們通常使用Fake對象在測試中來模擬真實(shí)的對象。 在測試中經(jīng)常會(huì)發(fā)現(xiàn)我們需要使用系統(tǒng)或者平臺(tái)給我們提供的接口,在測試中我們可以新創(chuàng)建一個(gè)類去實(shí)現(xiàn)此接口,然后在根據(jù)具體情況去實(shí)習(xí)此模擬類的相應(yīng)方法。 

          如我們創(chuàng)建了自己的FakeLog對象來模擬真實(shí)的日志打印,這樣我們可以在測試類中使用FakeLog來代替代碼中真實(shí)使用的Log類,可以通過FakeLog的方法和預(yù)期的結(jié)果比較來進(jìn)行測試正確性的判斷。

          Fake對象和mock對象還有一個(gè)實(shí)際中使用的區(qū)別,F(xiàn)ake對象我們構(gòu)造好后,以后所有的代碼都去調(diào)用此Fake對象就可以了,不用每個(gè)類每次都要給預(yù)期。從這個(gè)角度可以看到當(dāng)一個(gè)類的方法或者預(yù)期相對不變時(shí),可以采用Fake對象,當(dāng)這個(gè)類的返回信息預(yù)期變化非常不可預(yù)期時(shí),可以采用MOCK對象。

          3.4Mock服務(wù)的兩種方式

          (1)直接注入:用于類之間的依賴層次較多的情況,測試整個(gè)業(yè)務(wù)流程,粒度大。

          ResourceServerService service = mock(ResourcePPUServerService.class);
          new Processor().process(service );

          (2)重寫protected方法返回mock對象:用于類直接依賴于該服務(wù)的情況,測試行為的細(xì)節(jié),粒度小。

          ResourceServerService service = mock(ResourceServerService .class);
          generator = new EutranAnrDeletingItemGenerator() {
              @Override
              protected ResourceServerService getService() {
                  return service;
              }
          }

          3.5測試異常

          Throwable有兩個(gè)直接子類:Exception和Error

          1、expcetd=SomeExecption.class

          @Test(expected = AssertionError.class)
          public void should_occur_assertion_error_when_emb_number_is_not_eutran_or_utran_anr_delete() throws Exception 
          {
              EMBObject eMBObject = new EMBObject();
              new AnrDeleteProcessor().getAnrDeleteGenerator(EMBObject);
          }

          @Test(expected = NumberFormatException.class)
          public void should_throw_number_format_exception_when_input_string_field_greater_255() 
          {
           TransactionIDConvert.convertTransIDToLong(transactionError);
          }

          2、try-catch-fail只能用于Exception,Error不能用此種方式

          try {
              method.invoke();
              fail();
          catch (Exception e) {
           assertTrue(e.getCause() instanceof RuntimeException);
          }

          3.6私有方法—采用反射來調(diào)用

          @Test
          public void should_throw_runtime_exception_when_check_eutran_trap_data_fail() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
              when(eutranAnrAddItemGenerator.getSrvCelProcessor()).thenReturn(processor);
              when(processor.validateTrapData(any(AnrItem.class), any(AnrBean.class))).thenReturn(false);

              Method method = EutranAnrAddItemGenerator.class.getDeclaredMethod("check", AnrItem.class);
              method.setAccessible(true);
              try {
                  method.invoke(eutranAnrAddItemGenerator, anrAddItem);
              } catch (Exception e) {
                  assertTrue(e.getCause() instanceof RuntimeException);
              }
          }

          4.單元測試的格式

          4.1測試類結(jié)構(gòu)

          public class ExampleTest {
              @BeforeClass
              public static void setUp() throws Exception {
                  initGlobalParameter();
                  registerServices();
              }

              @Before
              public void setUp() throws Exception {
                  initGlobalParameter();
                  registerServices();
              }

              @After
              public void tearDown() throws Exception {
                  ServiceAccess.serviceMap.clear();
                  clearCache();     }

              @AfterClass
              public static void tearDown() throws Exception {
                  ServiceAccess.serviceMap.clear();
                  clearCache();
              }

              @Test
              public void should_get_some_result1_when_give_some_condition1{
              }

              @Test
              public void should_get_some_result2_when_give_some_condition2{
              }
          }

          JUnit4是JUnit框架有史以來的最大改進(jìn),其主要目標(biāo)便是利用Java5的Annotation特性簡化測試用例的編寫。先簡單解釋一下什么是Annotation,這個(gè)單詞一般是翻譯成元數(shù)據(jù)。元數(shù)據(jù)是什么?元數(shù)據(jù)就是描述數(shù)據(jù)的數(shù)據(jù)。也就是說,這個(gè)東西在Java里面可以用來和public、static等關(guān)鍵字一樣來修飾類名、方法名、變量名。修飾的作用描述這個(gè)數(shù)據(jù)是做什么用的,差不多和public描述這個(gè)數(shù)據(jù)是公有的一樣。

          • @Before:每個(gè)測試方法執(zhí)行之前都要執(zhí)行一次。
          • @After:before對應(yīng),每個(gè)測試方法執(zhí)行之后要執(zhí)行一次。
          • @BeforeClass:在所有測試方法之前運(yùn)行,只運(yùn)行一次。一般在此類中申請昂貴的外部資源。父類中有@BeforeClass方法,在其子類運(yùn)行之前也會(huì)運(yùn)行。
          • @AfterClass:與BeforeClass對應(yīng),在所有測試結(jié)束后,釋放BeforeClass中申請的資源。 注意:@Before,@After,@BeforeClass,@AfterClass 標(biāo)示的方法一個(gè)類中只能各有一個(gè)
          • @Test: 告訴JUnit,該方法要作為一個(gè)測試用例來運(yùn)行。

          4.2測試代碼的位置

          在Java中一個(gè)包可以橫跨兩個(gè)不同的目錄,所以我們的測試代碼和產(chǎn)品代碼放在同一目錄中,這樣維護(hù)起來更方便,測試代碼和產(chǎn)品代碼在同一個(gè)包中,這樣也減少了不必要的包引起,同時(shí)在測試類中使用繼承更加的方便。

          4.3測試用例格式3段式

          一個(gè)測試用例主體內(nèi)容一般采用三段式:given-when-then

          • Given:構(gòu)造測試條件;

          • When:執(zhí)行待測試的方法;

          • Then:判斷測試結(jié)果是否符合期望。 例如:

          @Test
          public void should_get_correct_result_when_add_two_numbers() {
           int a = 1;
           int b = 2;

           int c = MyMath.add(a, b);

           assertEquals(3, c);
          }

          4.4類名的命名方式

          測試類的名稱以Test結(jié)尾。從目標(biāo)類的類名衍生出其單元測試類的類名。類名前加上Test后綴。 Fake(偽類)放在測試包中,使用前綴Fake。

          4.5方法名的定義方式

          should …do something…when…under some conditions…

          例如:

          should_NOT_delete_A_when_exists_B_related_with_A
          should_throw_exception_when_the_parameter_is_illegal

          4.6業(yè)務(wù)代碼中為測試提供的方法的注解

          在業(yè)務(wù)代碼中為了測試而單獨(dú)提供的保護(hù)方法或者其他方法,我們通過@ForTest來標(biāo)注。FofTest類如下:

          @Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
          public @interface ForTest {
           String description() default "";
          }

          5.代碼中涉及外部接口時(shí),如何來編寫單元測試

          我們的代碼涉及的模塊非常眾多,經(jīng)常需要相互協(xié)作來完成一個(gè)功能,在此過程中經(jīng)常需要使用到外部的接口、同時(shí)也為別的模塊提供服務(wù)。

          5.1數(shù)據(jù)庫

          數(shù)據(jù)庫的單元測試,由于測試無法進(jìn)行數(shù)據(jù)庫的連接,故我們通過提取通用接口(DBManagerInterface)和FakeDBManager來實(shí)現(xiàn)數(shù)據(jù)庫解耦。FakeDBManager可以對真實(shí)的數(shù)據(jù)庫進(jìn)行模擬,也就是我們通過Fake一個(gè)簡單的內(nèi)存數(shù)據(jù)庫來模擬實(shí)際真實(shí)的數(shù)據(jù)庫。 DBManager是我們的真實(shí)連接數(shù)據(jù)庫的業(yè)務(wù)類。我們在測試時(shí),是可以通過注入的方式用FakeDBManager來替換DBManager。

          5.2平臺(tái)接口

          5.2.1 平臺(tái)接口的Mock

          平臺(tái)中的MinosMmlPPUServerService、ResourcePPUServerService等服務(wù)接口,都可以通過mock來進(jìn)行測試。需要注意的是在業(yè)務(wù)代碼中需要進(jìn)行相應(yīng)的解耦,可以通過SET方法或者構(gòu)造器來注入平臺(tái)的服務(wù)類。

          public class ICMEMBMessageListenerTest {
              private MinosMmlPPUServerService  minosMmlPPUServerService = mock(MinosMmlPPUServerService.class);

          @Before
          public void setUp() throws Exception {
              registerServices();
              icmembMessageListener = new ICMEMBMessageListener(){
              };
              when(minosMmlPPUServerService.getIp()).thenReturn("127.0.0.1");
              when(minosMmlPPUServerService.getPort()).thenReturn("80");
              when(minosMmlPPUServerService.getEmbPort()).thenReturn("8080");
          }

          此處需要注意如果用到靜態(tài)變量全局唯一的,需要在使用后在 tearDown中進(jìn)行清除。

          5.3 文件接口的測試

          我們的業(yè)務(wù)中也會(huì)出現(xiàn)與外部文件進(jìn)行讀寫的代碼。按照單元測試書寫的原則,單元測試應(yīng)該是獨(dú)立的,不依賴于外部任何文件或者資源的。好的單元測試是運(yùn)行速度快,能夠幫助我們定位問題。所以我們普通涉及到外部文件的代碼,都需要通過mock來預(yù)期其中的信息,如MOCK(I18n)文件或者properties、xml文件中的數(shù)據(jù)。 對于一些重要的文件,考慮到資源消耗不大的情況下,我們也會(huì)去為這些文件添加單元測試。需要訪問真實(shí)的文件,我們第一步就需要去獲取資源文件的具體位置。通過下面的FileService的getFileWorkDirectory我們可以獲取單元測試運(yùn)行時(shí)的根目錄。

          public class FileService {
          public static String getFileWorkDirectory() {
              return new StringBuilder(getFileCodeRootDirectory()).append("test").toString();
          }

          public static String getFileCodeRootDirectory() {
              String userDir = System.getProperty("user.dir");
              userDir = userDir.substring(0, userDir.indexOf(File.separator + "CODE" + File.separator));
              StringBuilder workFilePath = new StringBuilder(userDir);
              workFilePath.append(File.separator).append("CODE").append(File.separator);
              return workFilePath.toString();
          }
          }

          我們在單元測試中可以通過傳入具體的文件名稱,可以在測試代碼中訪問真實(shí)的文件。 這種方法可以適用I18n文件,xml文件, properties文件。 我們在對I18n文件進(jìn)行測試時(shí),也可以通過Fake對象根據(jù)具體的語言來進(jìn)行國際化信息的測試。具體FakeI18nWrapper的代碼在第7章中給出可以參考。

          @Before
          public void setUp() throws Exception {
              String i18nFilePath = FileService.getFileWorkDirectory() + "\\conf\\i18n.xml";
              I18N i18N = new FakeI18nWrapper(new File(i18nFilePath), I18nLanguageType.en_US);
              I18nAnrOsf.setTestingI18NInstance(i18N);
          }

          6.單元測試中涉及多線程、單例類、靜態(tài)類的處理

          6.1多線程測試

          通過單元測試,能較早地發(fā)現(xiàn) bug 并且能比不進(jìn)行單元測試更容易地修復(fù)bug。但是普通的單元測試方法(即使當(dāng)徹底地進(jìn)行了測試時(shí))在查找并行 bug 方面不是很有效。這就是為什么在實(shí)驗(yàn)室測試沒有問題,但在外場經(jīng)常出現(xiàn)各種莫名其妙的問題。 

          為什么單元測試經(jīng)常遺漏并行 bug?通常的說法是并行程序和Bug的問題在于它們的不確定性。但是對于單元測試目的而言,在于并行程序是非常 確定的。所以我們單元測試需要對關(guān)鍵的邏輯、涉及到并發(fā)的場景進(jìn)行多線程測試。 

          多線程的不確定性和單元測試的確定的預(yù)期確實(shí)是有點(diǎn)矛盾,這就需要精心的設(shè)計(jì)單元測試中的多線程用例。 Junit本身是不支持普通的多線程測試的,這是因?yàn)镴unit的底層實(shí)現(xiàn)上是用System.exit退出用例執(zhí)行的。JVM都終止了,在測試線程啟動(dòng)的其他線程自然也無法執(zhí)行。

          所以要想編寫多線程Junit測試用例,就必須讓主線程等待所有子線程執(zhí)行完成后再退出。我們一般的方法是在主測試線程中增加sleep方法,這種方法優(yōu)點(diǎn)是簡單,但缺點(diǎn)是不同機(jī)器的配置不一樣,導(dǎo)致等待時(shí)間無法確定。

          更為高效的多線程單元測試可以使用JAVA的CountDownLatch和第三方組件GroboUtils來實(shí)現(xiàn)。 下面通過一個(gè)簡單的例子來說明下多線程的單元測試。 測試的業(yè)務(wù)代碼如下,功能是唯一事務(wù)號(hào)的生成器。

          class UniqueNoGenerator {
              private static int generateCount = 0;

              public static synchronized int getUniqueSerialNo() {
                  return generateCount++;
              }
          }

          6.1.1 Sleep

          private static Setresults = new HashSet<>();

          @Test
          public void should_get_unique_no() throws InterruptedException {
              Thread[] threads = new Thread[100];
              for (int i = 0; i < threads.length; i++) {
              threads[i] = generateThread();
              }
              //啟動(dòng)線程
              Arrays.stream(threads).forEach(Thread::start);
              Thread.sleep(100L);
              
              assertEquals(results.size(), 100);
           }

          private Thread generateThread() {
              return new Thread(() -> {
                  int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
                  results.add(uniqueSerialNo);
              });
          }

          通過Sleep來等待測試線程中的所有線程執(zhí)行完畢后,再進(jìn)行條件的預(yù)期。問題就是用戶無法準(zhǔn)確的預(yù)期業(yè)務(wù)代碼線程執(zhí)行的時(shí)間,不同的環(huán)境等待的時(shí)間也是不等的。由于需要添加延時(shí),同時(shí)也違背了我們單元測試執(zhí)行時(shí)間需要盡量短的原則。

          6.1.2 ThreadGroup

          private static Set<Integer> results = new HashSet<>();
          private ThreadGroup threadGroup = new ThreadGroup("test");

          @Test
          public void should_get_unique_no() throws InterruptedException {
              Thread[] threads = new Thread[100];
              for (int i = 0; i < threads.length; i++) {
              threads[i] = generateThread();
           }
              //啟動(dòng)線程
              Arrays.stream(threads).forEach(Thread::start);
              while (threadGroup.activeCount() != 0) {
              Thread.sleep(1);
              }
              assertEquals(results.size(), 100);
              }
              
              private Thread generateThread() {
              return new Thread(threadGroup, () -> {
              int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
              results.add(uniqueSerialNo);
              });
          }

          這個(gè)是通過ThreadGroup來實(shí)現(xiàn)多線程測試的,可以把需要測試的類放入一個(gè)線程組,同時(shí)去判斷線程組中是否還有未結(jié)束的線程。測試中需要注意把新建的線程加入到線程組中。

          6.1.3 CountDownLatch

          private static Set<Integer> results = new HashSet<>();
          private CountDownLatch countDownLatch = new CountDownLatch(100);

          @Test
          public void should_get_unique_no() throws InterruptedException {
              Thread[] threads = new Thread[100];
              for (int i = 0; i < threads.length; i++) {
                  threads[i] = generateThread();
              }
              //啟動(dòng)線程
              Arrays.stream(threads).forEach(Thread::start);
              countDownLatch.await();

              assertEquals(results.size(), 100);
          }

          private Thread generateThread() {
              return new Thread(() -> {
                  int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
                  results.add(uniqueSerialNo);
                  countDownLatch.countDown();
              });
          }

          通過JAVA的CountDownLatch可以很方便的來判斷,測試中的線程是否已經(jīng)執(zhí)行完畢。CountDownLatch是一個(gè)同步輔助類,在完成一組正在其他線程中執(zhí)行的操作之前,它允許一個(gè)或多個(gè)線程一直等待,我們這里是讓測試主線程等待。countDown方法是當(dāng)前線程調(diào)用此方法,則計(jì)數(shù)減一。awaint方法,調(diào)用此方法會(huì)一直阻塞當(dāng)前線程,直到計(jì)時(shí)器的值為0。

          6.2單例類測試

          單例模式要點(diǎn):

          1. 單例類在一個(gè)容器中只有一個(gè)實(shí)例。
          2. 單例類使用靜態(tài)方法自己提供向客戶端提供實(shí)例,自己擁有自己的引用。
          3. 必須向整個(gè)容器提供自己的實(shí)例。 單例類的實(shí)現(xiàn)方式有多種方式,如懶漢式單例、餓漢式單例、登記式單例等。我們這里采用內(nèi)部類的形式來構(gòu)造單例類,實(shí)現(xiàn)的優(yōu)點(diǎn)是此種方式不需要給類或者方法添加鎖,唯一實(shí)例的生成是由JAVA的內(nèi)部類生成機(jī)制保證。 下面的例子構(gòu)造了一個(gè)單例類,同時(shí)這個(gè)單例類我們提供了一個(gè)獲取遠(yuǎn)程Cpu信息的方法。再構(gòu)造一個(gè)使用類ResourceManager.java來模擬調(diào)用此單例類,同時(shí)看下我們測試ResourceManager.java過程中遇到的問題。 單例類DBManagerTools.java:
          public class DbManager {
             private DbManager() {
             }
             
             public static DbManager getInstance() {
             return DbManagerHolder.instance;
             }
             
             private static class DbManagerHolder {
             private static DbManager instance = new DbManager();
             }
             
             public String getRemoteCpuInfo(){
             FtpClient ftpClient = new FtpClient("127.0.0.1","22");
             return ftpClient.getCpuInfo();
             }
            }

          調(diào)用類 ResourceManager.java:

          public class ResourceManager {
              public String getBaseInfo() {
                  StringBuilder buffer = new StringBuilder();
                  buffer.append("IP=").append("127.0.0.1").append(";CPU=").append(DbManager.getInstance().getRemoteCpuInfo());
                  return buffer.toString();
              }
          }

          測試類 
          @Test
          public void should_get_cpu_info() {
              String expected = "IP=127.0.0.1;CPU=Intel";
              ResourceManager resourceManager = new ResourceManager();

              String baseInfo = resourceManager.getBaseInfo();

              assertThat(baseInfo, is(expected));
          }

          從上面的描述可以看到,由于業(yè)務(wù)代碼強(qiáng)關(guān)聯(lián)了一個(gè)單例類,同時(shí)這個(gè)單例類會(huì)去通過網(wǎng)絡(luò)獲取遠(yuǎn)程機(jī)器的信息。這樣我們的單元測試在運(yùn)行中就會(huì)去連接網(wǎng)絡(luò)中的服務(wù)器導(dǎo)致測試失敗。在業(yè)務(wù)類中類似這種涉及到單例類的調(diào)用經(jīng)常用到。 這種情況下我們需要修改下業(yè)務(wù)代碼使代碼可測。 第一種方法:提取方法并在測試類中復(fù)寫。

          public class ResourceManager {
              public String getBaseInfo() {
                  StringBuilder buffer = new StringBuilder();
                  buffer.append("IP=").append("127.0.0.1").append("CPU=").append(getRemoteCpuInfo());
                  return buffer.toString();
              }

              @ForTest
              protected String getRemoteCpuInfo() {
                  return DbManager.getInstance().getRemoteCpuInfo();
              }
          }

          @Test
          public void should_get_cpu_info() {
              String expected = "IP=127.0.0.1;CPU=Intel";
              ResourceManager resourceManager = new ResourceManager(){
                  @Override
                  protected String getRemoteCpuInfo() {
                      return "Intel";
                  }
              };

              String baseInfo = resourceManager.getBaseInfo();

              assertThat(baseInfo, is(expected));
          }

          第二種方法:提取單例類中的方法為接口,然后在業(yè)務(wù)代碼中通過set方法或者構(gòu)造器注入到業(yè)務(wù)代碼中。

          public class DbManager implements ResourceService{
              private DbManager() {
              }

              public static DbManager getInstance() {
                  return DbManagerHolder.instance;
              }

              private static class DbManagerHolder {
                  private static DbManager instance = new DbManager();
              }

              @Override
              public String getRemoteCpuInfo(){
                  FtpClient ftpClient = new FtpClient("127.0.0.1","22");
                  return ftpClient.getCpuInfo();
              }

          public interface ResourceService {
           String getRemoteCpuInfo();
          }

          public class ResourceManager {
              private ResourceService resourceService = DbManager.getInstance();

              public String getBaseInfo() {
                  StringBuilder buffer = new StringBuilder();
                  buffer.append("IP=").append("127.0.0.1").append("CPU=").append(resourceService.getRemoteCpuInfo());
                  return buffer.toString();
              }

              public void setResourceService(ResourceService resourceService) {
                  this.resourceService = resourceService;
              }
          }

          @Test
          public void should_get_cpu_info() {
              String expected = "IP=127.0.0.1;CPU=Intel";
              ResourceManager resourceManager = new ResourceManager();
              DbManager mockDbManager = mock(DbManager.class);
              resourceManager.setResourceService(mockDbManager);
              when(mockDbManager.getRemoteCpuInfo()).thenReturn("Intel");
              
              String baseInfo = resourceManager.getBaseInfo();

              assertThat(baseInfo, is(expected));
          }

          通過上面的方法可以方便的解開業(yè)務(wù)代碼對單例的強(qiáng)依賴,有時(shí)候我們發(fā)現(xiàn)我們的業(yè)務(wù)代碼是靜態(tài)類,這個(gè)時(shí)候你會(huì)發(fā)下第一種方法是解決不了問題的,只能通過第2中方法來實(shí)現(xiàn)。 通過上面的代碼可以看到我們應(yīng)該盡量的少用單例,在必須使用單例時(shí)可以設(shè)計(jì)接口來進(jìn)行業(yè)務(wù)與單例類的解耦。

          6.3靜態(tài)類測試

          靜態(tài)類與單例類類似,也可以通過提取方法后通過復(fù)現(xiàn)方法來解耦,同樣也可以通過服務(wù)注入的方式來實(shí)現(xiàn)。也可以使用PowerMock來預(yù)期方法的返回。 實(shí)際應(yīng)用中如果單例類不需要維護(hù)任何狀態(tài),僅僅提供全局訪問的方法,這種情況考慮可以使用靜態(tài)類,靜態(tài)方法比單例更快,因?yàn)殪o態(tài)的綁定是在編譯期就進(jìn)行的。 

          同時(shí)需要注意的是不建議在靜態(tài)類中維護(hù)狀態(tài)信息,特別是在并發(fā)環(huán)境中,若無適當(dāng)?shù)耐酱胧┒薷亩嗑€程并發(fā)時(shí),會(huì)導(dǎo)致壞的競態(tài)條件。 單例與靜態(tài)主要的優(yōu)點(diǎn)是單例類比靜態(tài)類更具有面向?qū)ο蟮哪芰?,使用單例,可以通過繼承和多態(tài)擴(kuò)展基類,實(shí)現(xiàn)接口和更有能力提供不同的實(shí)現(xiàn)。 在我們開發(fā)過程中考慮到單元測試,還是需要謹(jǐn)慎的使用靜態(tài)類和單例類。

          7.代碼可測性的解耦方法

          在使用一些解依賴技術(shù)時(shí),我們常常會(huì)感覺到許多解依賴技術(shù)都破壞了原有的封裝性。但考慮到代碼的可測性和質(zhì)量,犧牲一些封裝性也是可以的,封裝本身也并不是最終目的,而是幫助理解代碼的。下面在介紹下常用的解依賴方法。這些解依賴方法的思想都是通用的,采用控制反轉(zhuǎn)和依賴注入的方式來進(jìn)行。

          7.1盡量減少業(yè)務(wù)代碼與平臺(tái)代碼之間的耦合

          軟件開發(fā)中調(diào)用平臺(tái)服務(wù)查詢資源屬性的典型代碼:

          public class DataProceeor{
              private static final SomePlatFormService service = ServerService.lookup(SomePlatFormService.ID);
              public static CompensateData getAttributes(String name){
                  service.queryCompensate(name);
              }
          }

          這種代碼在實(shí)現(xiàn)上沒有問題,但是無法進(jìn)行單元測試(不啟動(dòng)軟件)。因?yàn)榇祟惣虞d時(shí)需要獲取平臺(tái)查詢資源相關(guān)的服務(wù),業(yè)務(wù)代碼與平臺(tái)代碼存在強(qiáng)耦合性。 在不破壞原有功能的基礎(chǔ)上對這段代碼做如下改造:

          1、引入實(shí)例變量和構(gòu)造器

          public class DataProceeor{
              private static final SomePlatformService service = ServerService.lookup(SomePlatformService.ID);
              private SomePlatformService _service;

              public DataProceeor(SomePlatformService service) {
                  _service = service;
              }

              public DataProceeor() {
                  _service = ServerService.lookup(SomePlatformService.ID);;
              }

              public CompensateData getAttributes(String name){
                  service.queryCompensate(name);
              }
          }

          2、增加新方法

          public CompensateData getSomeAttributes(String name){
           _service.queryCompensate(name);
          }

          3、查找代碼中所有用到方法getAttributes的地方,全部替換成getSomeAttributes。

          4、完成第3步后,刪除已經(jīng)無用的變量和方法。

          5、重命名引入的變量和方法,使其符合命名規(guī)范。

          public class DataProceeor{
              private SomePlatformService service;
              public DataProceeor(SomePlatformService service){
                  this.service = service;
              }

              public DataProceeor() {
                  service = ServerService.lookup(SomePlatformService.ID);;
              }

              public static CompensateData getAttributes(String name){
                  service.queryCompensate(name);
              }
          }

          6、增加對新方法的測試用例

          public class DataProcessorTest {
              private DataProceeor dataProceeor;
              private SomePlateService somePlateService;
              private Map<String, String> attributes;

              @Before
              public void setUp() throws Exception {
                  attributes.put("pci""1");
              }

              @Test
              public void should_get_attributes() {
                  somePlateService = mock(SomePlateService.class);
                  when(somePlateService.queryAttribue()).thenReturn(attributes);

                  dataProceeor = new DataProceeor();

                  CompensateData compensateData = dataProceeor.getAttributes("pci");
                  assertThat(compensateData.value(), is("1"));
                  assertThat(compensateData.value(), is("2"));
              }
          }

          運(yùn)行該測試用例,發(fā)現(xiàn)最后一句斷言沒有通過: 修改最后一句斷言為:assertThat(attributeValue+"", not("2")); 再次運(yùn)行測試,測試用例通過。

          7.2 擴(kuò)展平臺(tái)的部分類,實(shí)現(xiàn)測試的目的

          模式1中的例子查詢資源屬性時(shí)沒有設(shè)置過濾條件,事實(shí)上大多數(shù)處理都是依賴其他處理類:

          public class NotificationDispatcher {
              private static Logger logger = LoggerFactory.getLogger(NotificationDispatcher.class);

              public void processMessage(String notificationMsg) {
                  NotificationMsg notification = new Gson().fromJson(notificationMsg, NotificationMsg.class);
                  Map<String, String> sctpInfo;
                  try {
                      sctpInfo = new NotificationParser().parse(notification.getMessage());
                      logger.info("Parse notification xml success: " + sctpInfo);
                      NotificationProcessor processor = new NotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));
                      processor.process(sctpInfo);
                  } catch (Exception e) {
                      logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
                  }
              }
          }

          在本例中,查詢MOI的Filter是在getCellMoi方法內(nèi)部構(gòu)造出來的,我們可以嘗試給getCellMoi方法編寫測試用例: 測試用例沒有通過,問題出在哪里呢? Debug代碼發(fā)現(xiàn),在getCellMoi方法內(nèi)部構(gòu)造出來的Filter和我們在測試代碼中構(gòu)造的Filter并不是同一個(gè)對象。很自然地想到為Filter類編寫子類,并覆蓋其equals方法。 用自定義的Filter代替平臺(tái)的Filter:

          public String getCellMoi(String cellName){
              Filter filter = new SelfFilter(cellName);
              return getAttributers(filter,"moi");
          }

          修改后測試用例運(yùn)行通過。

          7.3 巧用protedted方法實(shí)現(xiàn)測試的注入

          在模式2中,由于Filter是在getCellMoi內(nèi)部構(gòu)造的,并且沒有euqals方法,導(dǎo)致無法測試。還可以用別的方法對其進(jìn)行改造。代碼示例如下: 1.提取protected方法buildFilter()

          public void processMessage(String notificationMsg) {
              UmeNotificationMsg umeNotificationMsg = new Gson().fromJson(notificationMsg, UmeNotificationMsg.class);
              Map<String, String> sctpInfo;
              try {
                  sctpInfo = new NotificationParser().parse(umeNotificationMsg.getMessage());
                  logger.info("Parse notification xml success: " + sctpInfo);
                  NotificationProcessor processor = getNotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));

                  processor.process(sctpInfo);
              } catch (Exception e) {
                  logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
              }
          }

          @ForTest
          protected NotificationProcessorFactory getNotificationProcessorFactory() {
               return new NotificationProcessorFactory();
          }

          2.在測試代碼中重寫getNotificationProcessorFactory方法

          @Before
          public void setUp() throws Exception {
              NotificationProcessorFactory notificationProcessorFactory = mock(NotificationProcessorFactory.class);
              notificationDispatcher = new NotificationDispatcher(){
                  @Override
                  protected NotificationProcessorFactory getNotificationProcessorFactory() {
                      return notificationProcessorFactory;
                  }
              };
          }

          運(yùn)行測試,可以通過。

          8、總結(jié)

          UT是開發(fā)人員的利器,是開發(fā)的前置保護(hù)傘,也是寫出健壯代碼的有力保證,總之一句話不會(huì)寫UT的開發(fā)不是好廚子。

          瀏覽 53
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人做爰A片AAA毛真人 | 黄色操逼视频小说 | 免费看日逼视频情侣 | 日 韩 福利 资源 | 国产一级A片久久久免费看快餐 |