程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> TDD實踐之實用主義

TDD實踐之實用主義

編輯:關於JAVA

1. 為溝通選擇語言

我們在一個海員管理系統的開發中遇到了問題,這個領域的專業術語我們很 難翻譯。即使勉強翻譯出了,也感覺辭不達意,無論是初看上去,還是過一段時 間再看都一頭霧水。比如,我們寫出了下面的測試用例:

public void test_should_return_NOT_pass_if_duty_higher_than_second_mate_or_second_ engineer_and_education_level_is_secondary_and_guraduated_after_2002_02 _01() {
  ……
}

但其中second mate/second engineer是什麼意思呢? secondary的education level具體又是什麼?

還有:

public void test_should_return_third_mate_course_for_jianxi_third_mate() {
  ……
}

jianxi_third_mate是什麼? 等等。

當然,我們可以制定一個術語表,請專業人士先幫我們翻譯好,然後在代碼 中遵循這個術語表。然而隨著需求的增加,術語層出不窮,並且有特定中國特色 的名詞根本就沒有對應的翻譯,於是這個問題就一直困擾著我們。

而直到有一天,在一次重構中我們把上面的第一個測試用例重命名了一下, 一切似乎突然間開朗了:

public void test_應該算未通過_if_職務高於二副二管輪_而_學歷只 是中專_並且_畢業時間晚於2002年2月1日() {
  ……
}

Team裡的人紛紛圍過來,看著這個跟需求描述裡的驗收條件幾乎一模一樣的 測試用例名稱,感受到一種前所未有的清澈。大家幾乎在幾秒鐘之內就做出了選 擇:這種形式是可以接受的,而且表達能力更強,交流效果不錯。

什麼?使用中文作為函數名?這似乎只是那些被主流輿論鄙視的"漢語編程"研 究者才搞的東西,我們一直就被教育離這些東西遠點,甚至漢語拼音都不推薦使 用,一個經常拿來做反面教材的例子就是數據庫表的列名使用漢語拼音,這被看 作不專業的表現。又或者,以後團隊中加入外國開發者怎麼辦?

幸運的是,我們是軟件工程師,不是計算機科學家。學術理論可以極端,而 工程一定是某種折衷。定理由自然界精確遵守,而工程卻是各種應力的人為平衡 。

具體到這個案例,讓我們正視現實:

團隊成員並不善長本項目領域的專業英語。

任何翻譯都會造成一定的信息損失,尤其在一些具有中國特色的領域,比如" 中專"翻譯為英語就很難像中文一樣簡潔直觀。

在可預見的將來,不會有老外加入開發團隊。

而選用中文卻能夠讓我們更好的堅持以下原則:

代碼除了完成功能, 另外一個重要的功能是交流。(我們選擇了對團隊來說 最有效的交流方式)

用測試用例的名字來描述需求。(用中文描述更精確, 易於理解)

當然我們也會失去一些東西,比如對上面提到的"應該堅持使用英文"原則的 放棄。在這裡我們認為放棄這條原則的收益大於損失。一種損失就是失去了學習 英文的機會,比如上面最後一個測試用例,用中文寫出來就是:

public void test_見習三副應該參加三副的培訓() {
  ……
}

或者有人會說:"見習"的英語是"intern",常用詞啊。然而系統中還有另外 一類角色,叫"實習三副"等,那才是"intern"。實習是實際動手,擔當實際的職 責;見習是只看不練,跟在後面觀摩學習。見習的英文單詞是"noviciate",並 不為項目組所熟悉,而我們也不再關心它。

總之,在實踐中應當權衡各種利弊,選擇對你來說最有效的方式。

2. 用大量測試來驅動

在項目組中曾經發生過自以為完成了某個特性,後來卻發現漏掉了某些驗收 條件,甚至是比較重要驗收條件的事情。而這也是TDD經常被質疑的一點,就是 如何保證測試的完備性。因為總是想一點寫一點,不經過深入的思考,不可避免 的會漏掉某些測試條件。

然而,TDD並不妨礙你深入思考,只是勸你在實現時步子不要太大,小步前進 獲得對問題的進一步認識以隨時調整設計和實現。不過今天不爭論TDD的哲學問 題,我們只是關注用什麼樣的實踐來消除對於TDD測試完備性的疑慮。

事實上,完全可以根據需求文檔,驗收條件,進行"深入思考",從一開始就 寫下所有能想到的單元測試用例,就跟測試人員在產品出來前就對著需求准備測 試用例一樣。

哦,等等,這還叫 TDD 嗎? TDD 所強調的小步前進,隨時應變哪裡去了?為 全面的測試用例所花費的大量努力豈不是有浪費的風險?

嗯,不錯,TDD強調小步前進的原因就是要避免浪費。如果我們能找到一種方 法,既能夠提醒我們不要忘記需求,又讓我們在需求變化時不致浪費太多,豈不 是皆大歡喜?

想想,我們用什麼來描述需求?是測試用例名稱,而不是測試用例的函數體, 而名稱的書寫幾乎是沒有成本的。從需求文檔中把驗收條件摳出來即可。如:

public void test_會被認為不服從調配_if_the_seaman_在當前職位 上曾經曠工() {
     // TODO
   }
   public void test_會被認為不服從調配_if_the_seaman_在當前職位上 曾經請過病假() {
     // TODO
   }
   public void test_會被認為不服從調配_if_the_seaman_在當前職位上 曾經請過事假() {
     // TODO
   }
   public void test_會被認為不服從調配_if_the_seaman_在當前職位上 曾經被遣返() {
     // TODO
   }
   public void test_不會被認為不服從調配_if_the_seaman_在當前職位 上從未曠工_請病假_請事假_和遣返() {
     // TODO
   }

兩分鐘,我們就把這個用戶故事的測試用例按照驗收條件裡說的全部描述出 來了。函數體全部都是空的,因此所有的測試都是通過的,不會強迫你一次性把 所有的測試都實現。

每次你流覽或修改這段代碼,空的函數體或裡面的// TODO都會提醒你還有測 試沒有完成。即使你不在這個特性上工作了,切換過來的Pair也會從你遺留的測 試用例名稱中迅速的了解需要做什麼。

這種方法是怎麼解決問題的呢?

第一,還是讓我們正視現實:如果需求描述不和代碼放在一起,開發人員很 少會在開發過程中去翻閱需求文檔,甚至是特性編碼結束後。這在成熟的開發團 隊中會有改善,但仍然不可避免。把需求描述以測試用例名稱的方式放進代碼, 便會無時無刻不在提醒開發者,還有這個這個這個驗收條件沒滿足。

第二,我們依然堅持了以下原則:

用測試用例的名字來描述需求。

小步前進,編寫一個測試用例,實現一段產品代碼,編寫下一個測試用例, 實現下一段產品代碼。(因為所有的未完成測試都是通過的,不妨礙你運行測試 ,提交代碼和持續集成)

當實現過程中發現事情並不是當初想的那樣時,隨時更改或刪除之前寫的測 試用例,不會造成大的浪費。(因為只是函數名加空的函數體,成本很低)

在開始寫下"所有"的測試用例名稱並不意味著一勞永逸。中間當需求發生變 化,我們需要對應的添加或刪除一部分測試用例。在實現的過程中,發現某些條 件不在驗收范圍內,或許是之前沒考慮到,那麼跟BA/QA確認後,需要添加到用 例列表中。

(細心的讀者可能發現,這裡面存在著重復。就是以普通文本形式存在的驗收 條件和以測試用例名稱存在的驗收條件。或許應該有類似SVN2Wiki的工具,來消 除這類重復)

當然,完備性還有其它的含義和檢測條件,不是說你提前多寫幾個用例就是 完備了。這裡只是用一種成本最低的方式來解決問題。

3. 一個環境,多個斷言

隨著時間的推移,項目組發現測試運行的越來越慢。當然,這是一個普遍現 象,也已經有很多方法來加速測試的運行速度。但很多需要新工具的支持,而項 目組暫時沒時間去切換工具。有沒有其它更方便的做法?

像其它性能問題一樣,我們首先需要確定瓶頸在哪裡。我們發現主要是每個 測試用例運行前搭建測試所需的環境相對較為耗時,尤其是Selenium測試,它需 要啟動和關閉浏覽器。並且,很多測試用例其實使用相同的環境設置,只是每個 用例僅僅去斷言其中一個需求。

我們可以修改Runner,讓一組測試使用同一個浏覽器實例,每次環境的清理 通過清理Session來完成。而我們也可以采取另外一種方法,就是合並使用相同 環境設置的測試用例,把它們的斷言都放進同一個用例。

哦,這又違反了Kent Beck 為TDD制定的原則:每個測試用例最好只有一個斷 言。

好,讓我們再一次分析原則背後的理念。一個用例一個斷言,是為了讓測試 更清晰,更精確的描述需求,測試失敗更容易定位。那麼有沒有一種方法,既能 讓多個斷言共享相同的環境設置,又能清晰精確的描述需求呢?

想想,我們用什麼來描述需求?是測試用例的名稱,確切的說,是函數的名稱 。只要我們把一組組相關的斷言封裝到一個個函數裡,給它們一個能夠清晰精確 的描述這組斷言對應的需求的名稱,然後在測試用例裡面調用這些函數就可以了 。這樣我們只需為多個斷言設置一次環境,而同時又保留了清晰精確的表達需求 的能力。

@Test
   public void test_should_show_step_details_info_in_todo_item_page() throws Exception {
     TodoItemPage page = navigator.gotoTodoItemPage( );
     should_show_step_name_as_page_title (activeStepOfNonStartedInstance, page);
     should_show_start_processing_button_if_current_step_status_is_waiting (page);
     should_show_transition_buttons (activeStepOfNonStartedInstance, page);
     should_NOT_ask_user_to_input_his_opinion_if_current_step_status_is_NOT _processing(page);
     should_show_comment_box_after_click_start_process (page);
   }
   private void should_show_step_name_as_page_title(FlowStep step, TodoItemPage page) {
     assertEquals(step.getName(), page.title());
   }
   private void should_show_start_processing_button_if_current_step_status_is_waiting (TodoItemPage page) {
     assertTrue(page.isStartProcessingButtonVisible());
   }
   private void should_show_comment_box_after_click_start_process (TodoItemPage page) {
     page。clickStartProcessingButton();
     assertTrue(page.isCommentBoxAppear());
   }
   private void should_ask_user_to_input_his_opinion_if_current_step_status_is_process ing(TodoItemPage page) {
     assertTrue(page.isCommentBoxVisible());
     assertTrue(page.isActionButtonsVisible());
   }
   private void should_ask_user_to_select_next_step_operators (FlowTransitionDefinition nextTransitonOfStep,
                                TodoItemPage page) {
     assertTrue(page。isUserGroupVisible (nextTransitonOfStep.getId()));
   }
   ……

小結

回過頭來我們看看上面的三個實踐,它們如出一轍的,一次又一次的"違反" 了某種原則。它們分別是"不能用漢語","不能一次編寫多個測試用例",和"不 能在一個用例裡面使用多組斷言",而實際上,我們違反的只是這些原則的外在 形式,但卻堅持了這些原則背後的思想,如最有效的溝通,注重實效而不是形式 。以此為基石,我們可以在出現新的約束的情況下,靈活運用,發明各種實踐, 並享受由此帶來的效率提升。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved