测试驱动开发 测试前移_测试驱动陷阱,第2部分
測(cè)試驅(qū)動(dòng)開(kāi)發(fā) 測(cè)試前移
單元測(cè)試中單元的故事在本文的上半部分 ,您可能會(huì)看到一些不好但很受歡迎的測(cè)試示例。 但是我不是一個(gè)專(zhuān)業(yè)的批評(píng)家(也被稱為“巨魔”或“仇恨者”),沒(méi)有任何建設(shè)性的話就抱怨。 多年的TDD教給我的不僅僅是事情會(huì)變得多么糟糕。 有許多簡(jiǎn)單但有效的技巧,可以使您的測(cè)試生活變得更加輕松。
想象一下:您有一家小公司中一間小型會(huì)議室的預(yù)訂系統(tǒng)。 由于某些奇怪的原因,它必須處理離線預(yù)訂。 人們將他們的預(yù)訂請(qǐng)求發(fā)布到某個(gè)前端,每周一次,您會(huì)收到一個(gè)文本文件,其中包含公司的工作時(shí)間以及所有的預(yù)訂(在當(dāng)天,多長(zhǎng)時(shí)間,由誰(shuí),在什么時(shí)間點(diǎn)提交)訂購(gòu)。 您的系統(tǒng)應(yīng)根據(jù)一些業(yè)務(wù)規(guī)則(先到先得,僅在辦公室工作時(shí)間內(nèi)提供此類(lèi)服務(wù))為房間生成日歷。
作為分析的一部分,我們提供了明確定義的輸入數(shù)據(jù)和預(yù)期結(jié)果,并帶有示例。 確實(shí),TDD的情況很好。 可悲的是,在現(xiàn)實(shí)生活中從來(lái)沒(méi)有發(fā)生過(guò)這樣的事情。
我們的示例測(cè)試數(shù)據(jù)如下所示:
class TestData {static final String INPUT_FIRST_LINE = '0900 1730\n';static final String FIRST_BOOKING = '2011-03-17 10:17:06 EMP001\n' +'2011-03-21 09:00 2\n';static final String SECOND_BOOKING = '2011-03-16 12:34:56 EMP002\n' +'2011-03-21 09:00 2\n';static final String THIRD_BOOKING = '2011-03-16 09:28:23 EMP003\n' +'2011-03-22 14:00 2\n';static final String FOURTH_BOOKING = '2011-03-17 10:17:06 EMP004\n' +'2011-03-22 16:00 1\n';static final String FIFTH_BOOKING = '2011-03-15 17:29:12 EMP005\n' +'2011-03-21 16:00 3';static final String INPUT_BOOKING_LINES =FIRST_BOOKING +SECOND_BOOKING +THIRD_BOOKING +FOURTH_BOOKING +FIFTH_BOOKING;static final String CORRECT_INPUT = INPUT_FIRST_LINE + INPUT_BOOKING_LINES;static final String CORRECT_OUTPUT = '2011-03-21\n' +'09:00 11:00 EMP002\n' +'2011-03-22\n' +'14:00 16:00 EMP003\n' +'16:00 17:00 EMP004\n' +''; }現(xiàn)在,我們從一個(gè)積極的測(cè)試開(kāi)始:
BookingCalendarGenerator bookingCalendarGenerator = new BookingCalendarGenerator();@Test public void shouldPrepareBookingCalendar() {//whenString calendar = bookingCalendarGenerator.generate(TestData.CORRECT_INPUT);//thenassertEquals(TestData.CORRECT_OUTPUT, calendar); }看來(lái)我們已經(jīng)使用“生成”方法設(shè)計(jì)了BookingCalendarGenerator。 很公平。 讓我們添加更多測(cè)試。 測(cè)試業(yè)務(wù)規(guī)則。 我們得到這樣的東西:
@Testpublic void noPartOfMeetingMayFallOutsideOfficeHours() {//givenString tooEarlyBooking = '2011-03-16 12:34:56 EMP002\n' +'2011-03-21 06:00 2\n';String tooLateBooking = '2011-03-16 12:34:56 EMP002\n' +'2011-03-21 20:00 2\n';//whenString calendar = bookingCalendarGenerator.generate(TestData.INPUT_FIRST_LINE + tooEarlyBooking + tooLateBooking);//thenassertTrue(calendar.isEmpty());}@Testpublic void meetingsMayNotOverlap() {//givenString firstMeeting = '2011-03-10 12:34:56 EMP002\n' +'2011-03-21 16:00 1\n';String secondMeeting = '2011-03-16 12:34:56 EMP002\n' +'2011-03-21 15:00 2\n';//whenString calendar = bookingCalendarGenerator.generate(TestData.INPUT_FIRST_LINE + firstMeeting + secondMeeting);//thenassertEquals('2011-03-21\n' +'16:00 17:00 EMP002\n', calendar);}@Testpublic void bookingsMustBeProcessedInSubmitOrder() {//givenString firstMeeting = '2011-03-17 12:34:56 EMP002\n' +'2011-03-21 16:00 1\n';String secondMeeting = '2011-03-16 12:34:56 EMP002\n' +'2011-03-21 15:00 2\n';//whenString calendar = bookingCalendarGenerator.generate(TestData.INPUT_FIRST_LINE + firstMeeting + secondMeeting);//thenassertEquals('2011-03-21\n15:00 17:00 EMP002\n', calendar);}@Testpublic void orderingOfBookingSubmissionShouldNotAffectOutcome() {//givenList<String> shuffledBookings = newArrayList(TestData.FIRST_BOOKING, TestData.SECOND_BOOKING,TestData.THIRD_BOOKING, TestData.FOURTH_BOOKING, TestData.FIFTH_BOOKING);shuffle(shuffledBookings);String inputBookingLines = Joiner.on('\n').join(shuffledBookings);//whenString calendar = bookingCalendarGenerator.generate(TestData.INPUT_FIRST_LINE + inputBookingLines);//thenassertEquals(TestData.CORRECT_OUTPUT, calendar);}僅此而已。 但是,如果我們得到一些垃圾作為輸入怎么辦。 還是如果我們得到一個(gè)空字符串? 讓我們?yōu)榇嗽O(shè)計(jì):
@Test(expected = IllegalArgumentException.class)public void rubbishInputDataShouldEndWithException() {//whenString calendar = bookingCalendarGenerator.generate('rubbish');//then exception is thrown}@Test(expected = IllegalArgumentException.class)public void emptyInputDataShouldEndWithException() {//whenString calendar = bookingCalendarGenerator.generate('');//then exception is thrown}IllegalArgumentException很公平。 我們不需要再花哨的方式處理它。 我們已經(jīng)完成了。 最后,讓我們?cè)跍y(cè)試下編寫(xiě)該類(lèi):BookingCalendarGenerator。
因此,我們做到了。 結(jié)果表明,對(duì)于一個(gè)方法來(lái)說(shuō),整個(gè)過(guò)程有點(diǎn)大。 因此,我們使用了“提取方法”模式的強(qiáng)大功能。 我們將代碼片段分為不同的方法。 我們將可操作的方法和數(shù)據(jù)分組為類(lèi)。 我們使用面向?qū)ο缶幊痰墓δ?#xff0c;使用單一職責(zé)原理,使用組合(準(zhǔn)確地說(shuō)是分解),最后得到一個(gè)像這樣的程序包:
我們有一個(gè)公共類(lèi),和幾個(gè)包作用域類(lèi)。 這些包范圍類(lèi)顯然屬于公共類(lèi)。 為了清晰起見(jiàn),這是一個(gè)類(lèi)圖:
那些不是愚蠢的數(shù)據(jù)對(duì)象。 這些是成熟的課程。 具有行為,責(zé)任感和封裝感。 這是我們測(cè)試驅(qū)動(dòng)者可能想到的一件事:我們沒(méi)有針對(duì)這些類(lèi)的測(cè)試。 我們只有公共班級(jí)。 不好,對(duì)吧? 沒(méi)有測(cè)試一定是不好的。 很壞。 對(duì)?
錯(cuò)誤。
我們有測(cè)試。 我們啟動(dòng)了代碼覆蓋率工具,然后看到:100%的方法和類(lèi)。 95%的線。 不錯(cuò)(在下一篇文章中,我將達(dá)到不確定性的5%)。
但是我們只有一個(gè)單元測(cè)試類(lèi)。 這樣好嗎
好吧,讓我強(qiáng)調(diào)一下,指出答案:
這是一個(gè)UNIT測(cè)試。 有理由將其稱為UNIT測(cè)試!
該單元不必是一個(gè)單一的類(lèi)。 該單元不必是單個(gè)包裝。 由您決定單位。 這是一個(gè)通用名稱,因?yàn)槟睦碇呛统WR(shí)應(yīng)該告訴您停止的地方。
所以我們有六個(gè)班級(jí)作為一個(gè)單元,有什么大不了的? 除了其他人之外,是否有人要使用其中一個(gè)類(lèi)別呢? 他不會(huì)對(duì)此進(jìn)行測(cè)試,對(duì)嗎?
錯(cuò)誤。 除了在測(cè)試中實(shí)際調(diào)用的那些類(lèi)之外,這些類(lèi)都是包范圍的。 這個(gè)包裹范圍的事情告訴您:“退后一步。 不要碰我,我屬于這個(gè)包裹。 不要試圖單獨(dú)使用我,我是設(shè)計(jì)在這里!”。
因此,是的,如果程序員將其中之一或?qū)⑵涔_(kāi),他可能會(huì)知道,所有保證都將失效。 寫(xiě)你自己的測(cè)試,伙計(jì)。
我被問(wèn)到是否有人要向其中一個(gè)類(lèi)添加某些行為呢? 他怎么會(huì)知道他沒(méi)有破東西?
好吧,他將從測(cè)試開(kāi)始,對(duì)嗎? 是TDD,對(duì)不對(duì)? 如果需求有變化,則將此變化編碼為測(cè)試,然后,直到那時(shí),您才開(kāi)始弄亂代碼。 因此,您是安全的。
我看到人們盲目地寫(xiě)每堂課的測(cè)試,卻沒(méi)有思考,這讓我哭了。 我最近做了很多配對(duì)編程,您知道我發(fā)現(xiàn)了什么嗎? Java程序員通常不使用package-scope。 Java程序員通常不知道,這種保護(hù)意味著:對(duì)我來(lái)說(shuō),我的所有后代和每個(gè)人都在同一軟件包中。 沒(méi)錯(cuò),受保護(hù)不僅僅是包范圍,更不是一點(diǎn)點(diǎn)。 因此,如果Java程序員不知道包范圍實(shí)際上是什么,并且與Groovy相反,這是默認(rèn)值,那么他們?nèi)绾卫斫鈫卧鞘裁?#xff1f;
我能得到多高?
現(xiàn)在,有一個(gè)有趣的想法:如果我們可以對(duì)一個(gè)包進(jìn)行單個(gè)測(cè)試,那么我們可以對(duì)一個(gè)包樹(shù)進(jìn)行單個(gè)測(cè)試。 你知道,像這樣:
我們都知道Java中的軟件包不是真正的樹(shù)狀包,唯一具有目錄結(jié)構(gòu)的是按照非常古老的約定,而且我們知道目錄結(jié)構(gòu)僅用于解決名稱沖突問(wèn)題,盡管如此,我們還是傾向于使用包,就像name.after.the.dot有意義一樣。 就像我們可以將一個(gè)包裹隱藏在另一個(gè)包裹中一樣。 或與他們建立千層面的烤寬面條。
那么可以為一個(gè)樹(shù)形樹(shù)使用一個(gè)測(cè)試類(lèi)嗎?
是的。
但是,如果是這樣,到哪里結(jié)束? 我們可以從包樹(shù)一直到應(yīng)用程序的入口點(diǎn)嗎? 這些……可能是集成測(cè)試或功能測(cè)試。 我們能做到嗎? 那會(huì)好嗎?
答案是:可以。 在一個(gè)完美的世界中,那會(huì)很好。 在我們那骯臟的,懸掛在刀刃上的世界里,這簡(jiǎn)直是瘋了。 為什么? 因?yàn)楣δ苄缘亩说蕉藴y(cè)試很慢。 太慢了。 太慢了,以至于您想將它們?nèi)拥?#xff0c;然后到某個(gè)地方,而不必總是等待某些東西。 一個(gè)充滿創(chuàng)造力,不斷反饋和閃電般快速安全的地方。
您將返回單元測(cè)試。
甚至還有更多原因。 一種是,很難對(duì)應(yīng)用程序的所有流程進(jìn)行端到端測(cè)試。 您可能應(yīng)該對(duì)所有主要流程都執(zhí)行此操作,但是對(duì)于錯(cuò)誤,連接不良以及所有可能在一處或另一處拋出的棘手邏輯部分呢? 不,有時(shí)候像這樣設(shè)置集成測(cè)試環(huán)境會(huì)太困難了,所以最終還是要用單元測(cè)試來(lái)測(cè)試它。
第二個(gè)原因是,盡管功能測(cè)試并未在代碼上澆注具體的內(nèi)容,但不會(huì)通過(guò)在測(cè)試用例中重復(fù)執(zhí)行算法來(lái)抑制您的創(chuàng)造力,但是它們也沒(méi)有提供重構(gòu)的安全性。 當(dāng)您擁有一個(gè)具有單個(gè)公共類(lèi)的程序包時(shí),很明顯有人可以安全地做什么,而他不能做什么。 當(dāng)您將某些內(nèi)容包含在庫(kù)或插件中時(shí),它仍然很明顯。 但是,如果您有成千上萬(wàn)個(gè)公共類(lèi),并且要實(shí)現(xiàn)一個(gè)新功能,則可能要使用其中一些,并且您想知道它們很好。
因此,不,在我們的世界中,僅進(jìn)行功能測(cè)試是沒(méi)有意義的。 抱歉。 但是按類(lèi)創(chuàng)建測(cè)試也沒(méi)有意義。 出于某種原因,它被稱為UNIT測(cè)試。 用那個(gè)
祝您編程愉快,別忘了分享!
參考: 測(cè)試驅(qū)動(dòng)陷阱,來(lái)自我們JCG合作伙伴 Jakub Nabrdalik的第2部分 ,在Solid Craft博客上。
翻譯自: https://www.javacodegeeks.com/2012/09/test-driven-traps-part-2.html
測(cè)試驅(qū)動(dòng)開(kāi)發(fā) 測(cè)試前移
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的测试驱动开发 测试前移_测试驱动陷阱,第2部分的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 弹弹堂安卓版下载(弹弹堂安卓版)
- 下一篇: Oracle Service Bus简介