新聞中心
作者 | 張哲

創(chuàng)新互聯(lián)是一家集網(wǎng)站建設(shè),青神企業(yè)網(wǎng)站建設(shè),青神品牌網(wǎng)站建設(shè),網(wǎng)站定制,青神網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷,網(wǎng)絡(luò)優(yōu)化,青神網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
EasyModeling 是我在2021年圣誕假期期間開發(fā)的一個(gè) Java 注解處理器,采用 Apache-2.0 開源協(xié)議。它可以幫助 Java 單元測(cè)試的編寫者快速構(gòu)造用于測(cè)試的數(shù)據(jù)模型實(shí)例,簡(jiǎn)化 Java 項(xiàng)目在單元測(cè)試中準(zhǔn)備測(cè)試數(shù)據(jù)的工作,在提高編寫效率的同時(shí),使單元測(cè)試更加整潔易讀。經(jīng)過一年的維護(hù),EasyModeling 已經(jīng)在幾個(gè) Thoughtworks 內(nèi)部的項(xiàng)目上得到了應(yīng)用,并迭代發(fā)布了幾個(gè)版本。
單元測(cè)試中的數(shù)據(jù)準(zhǔn)備的困難
在企業(yè)級(jí)應(yīng)用軟件開發(fā)項(xiàng)目中編寫測(cè)試代碼時(shí),針對(duì)特定的測(cè)試場(chǎng)景,我們需要準(zhǔn)備相應(yīng)的測(cè)試數(shù)據(jù),以驗(yàn)證被測(cè)組件在給定輸入下的行為。在使用 Java 語言的項(xiàng)目中,這些準(zhǔn)備測(cè)試數(shù)據(jù)的代碼體現(xiàn)為創(chuàng)建各種“數(shù)據(jù)模型類”的實(shí)例。這里的數(shù)據(jù)模型類,可以包括聚合模型(Aggregation Model)、數(shù)據(jù)傳遞模型(DTO)、值對(duì)象(VO)以及存儲(chǔ)模型(Persist Model)等等。無論是對(duì)服務(wù)組件的測(cè)試,還是對(duì)數(shù)據(jù)模型本身的測(cè)試,我們都無可避免地需要構(gòu)建這些數(shù)據(jù)模型類的實(shí)例。
在項(xiàng)目的起初階段,準(zhǔn)備數(shù)據(jù)的工作是簡(jiǎn)單的,我們只需要調(diào)用數(shù)據(jù)模型類的構(gòu)造方法,傳入適當(dāng)?shù)膮?shù)來創(chuàng)建實(shí)例即可。單元測(cè)試代碼的規(guī)模不會(huì)太大,也尚且清晰易讀。
但是隨著產(chǎn)品開發(fā)工作的展開,一方面,項(xiàng)目中使用的這些數(shù)據(jù)模型會(huì)變得越來越復(fù)雜;另一方面,測(cè)試場(chǎng)景也會(huì)變得越來越多。經(jīng)驗(yàn)上,在經(jīng)過幾個(gè)版本迭代的企業(yè)級(jí)應(yīng)用 Java 代碼中,我們通常不難找出一些擁有十幾個(gè)、甚至幾十個(gè)成員變量的數(shù)據(jù)模型類,并且它們之間還存在著復(fù)雜的相互持有、嵌套、繼承的關(guān)系。這些數(shù)據(jù)模型類往往都是項(xiàng)目中的核心組件,故而也成為單元測(cè)試需要重點(diǎn)關(guān)注的組件。相應(yīng)地,在涉及這些數(shù)據(jù)模型的單元測(cè)試中,為準(zhǔn)備測(cè)試數(shù)據(jù)而編寫的初始化數(shù)據(jù)模型類的代碼量也會(huì)越來越大、越來越復(fù)雜。
這些冗雜繁復(fù)的數(shù)據(jù)初始化代碼會(huì)影響單元測(cè)試本身的代碼質(zhì)量,造成單元測(cè)試編寫成本高、易讀性差、易維護(hù)性低等問題。而單元測(cè)試的質(zhì)量又與生產(chǎn)代碼的質(zhì)量息息相關(guān)。例如,單元測(cè)試的編寫成本過高,會(huì)使開發(fā)者越來越傾向于僅在已有測(cè)試基礎(chǔ)上做修改,而不是為每個(gè)場(chǎng)景創(chuàng)建單獨(dú)的測(cè)試,造成單個(gè)測(cè)試的職責(zé)過多;甚至使開發(fā)者放棄單元測(cè)試,降低了團(tuán)隊(duì)對(duì)產(chǎn)品質(zhì)量的信心。又比如,單元測(cè)試的易讀性差,導(dǎo)致單元測(cè)試無法承擔(dān)起“測(cè)試即文檔(tests as documentation)”的職責(zé)。而單元測(cè)試的易維護(hù)性低,則導(dǎo)致了代碼很難被重構(gòu),從而單元測(cè)試不僅沒有為重構(gòu)提供信心,反而變成重構(gòu)的桎梏。
具體來說,這些初始化數(shù)據(jù)的代碼會(huì)引起三個(gè)方面的問題:
- 對(duì)測(cè)試場(chǎng)景的描述不清晰
- 構(gòu)建測(cè)試數(shù)據(jù)的代碼重復(fù)
- 初始化數(shù)據(jù)模型代碼的膨脹
我們可以從下面的例子中略窺端倪。你是否在你的項(xiàng)目中見過這樣的單元測(cè)試?
圖片
這是一段典型的使用JUnit測(cè)試框架的單元測(cè)試代碼。在這段單元測(cè)試代碼中,被測(cè)對(duì)象是 leaveCalculator 組件的 annualLeave 方法。我們首先創(chuàng)建一位員工,如(a)處;然后將創(chuàng)建好的員工對(duì)象傳入 annualLeave 方法,為其計(jì)算出應(yīng)得的年假數(shù)額,如(2)處;最后斷言他應(yīng)該享有20天年假,如(3)處。為了簡(jiǎn)化討論,我們暫且假設(shè)此處 annualLeave 方法的業(yè)務(wù)規(guī)則是:?jiǎn)T工應(yīng)得的年假數(shù)額只與這位員工加入公司的時(shí)間(date of joining)相關(guān),即在代碼中 (1) 處初始化的日期。
我們來詳細(xì)分析這段測(cè)試代碼中存在的壞味道、以及其潛在的問題。
對(duì)測(cè)試場(chǎng)景的描述不清晰
如前文所述,我們假設(shè)這段單元測(cè)試代碼的目的是驗(yàn)證“入職超過5年的員工應(yīng)該享有20天年假”這個(gè)業(yè)務(wù)規(guī)則。那么顯然,其中只有 (1), (2), (3) 這三處是與當(dāng)前測(cè)試場(chǎng)景相關(guān)的,它們共同構(gòu)成了對(duì)上述業(yè)務(wù)規(guī)則的描述。而在 (1) 處之前傳入 Employee 類構(gòu)造方法的那些參數(shù)都是與當(dāng)前測(cè)試場(chǎng)景無關(guān)的。遺憾的是,這些與測(cè)試場(chǎng)景無關(guān)的代碼卻占據(jù)了這個(gè)代碼片段中的絕大部分代碼行。
在實(shí)際項(xiàng)目中,我們會(huì)見到很多這樣的單元測(cè)試,它們往往需要用幾十行的代碼來準(zhǔn)備復(fù)雜的測(cè)試數(shù)據(jù),需要初始化數(shù)個(gè)數(shù)據(jù)模型類的對(duì)象,以支持對(duì)被測(cè)組件的調(diào)用,然而這些代碼中真正在描述測(cè)試場(chǎng)景的,卻只有其中區(qū)區(qū)幾行、甚至一兩行。這不僅增加了測(cè)試的篇幅,還會(huì)導(dǎo)致閱讀者無法快速聚焦在有意義的初始化條件上。就像我們?cè)谶@個(gè)例子中看到的,描述測(cè)試場(chǎng)景的代碼行(1)處混雜在大量初始化測(cè)試數(shù)據(jù)的代碼行之中,造成了單元測(cè)試對(duì)測(cè)試場(chǎng)景的描述不聚焦。這使單元測(cè)試的閱讀者很難從這段測(cè)試代碼中一目了然地理解測(cè)試的意圖,更遑論以測(cè)試為文檔來理解業(yè)務(wù)規(guī)則。而在測(cè)試失敗時(shí),也無法快速?gòu)臏y(cè)試場(chǎng)景的數(shù)據(jù)構(gòu)造出發(fā)去定位問題。
一些有經(jīng)驗(yàn)的單元測(cè)試編寫者已經(jīng)注意到了這個(gè)問題,他們會(huì)在關(guān)鍵的測(cè)試數(shù)據(jù)初始化行末添加一些注釋以示強(qiáng)調(diào)。然而注釋本身就預(yù)示著代碼壞味道,并且在重構(gòu)中也是非常不安全的,甚至反而誤導(dǎo)讀者。
構(gòu)建測(cè)試數(shù)據(jù)的代碼重復(fù)
如果將目光從單個(gè)測(cè)試放大到單元測(cè)試組(Test Suit),我們會(huì)發(fā)現(xiàn)在針對(duì)同一個(gè)被測(cè)組件的不同測(cè)試場(chǎng)景下,初始化數(shù)據(jù)模型的代碼會(huì)大量重復(fù)。例如在針對(duì)員工年假數(shù)額計(jì)算(leaveCalculator 組件的 annualLeave 方法)的測(cè)試組中,假設(shè)按照業(yè)務(wù)規(guī)則,我們需要考慮以下的測(cè)試場(chǎng)景:
- 入職不足2年的員工,應(yīng)該享有10天年假;
- 當(dāng)年入職的員工,享有按照入職時(shí)間折算的年假數(shù)額;
- 入職超過2年,而不足5年的員工,應(yīng)該享有15天年假;
- 入職超過5年的員工,應(yīng)該享有20天年假;
- 入職超過7年的員工,應(yīng)該享有25天年假;
- 入職時(shí)間在未來(尚未入職)的員工,不應(yīng)該計(jì)算年假數(shù)額(拋出異常);
不難想象,我們會(huì)分別在這6個(gè)測(cè)試場(chǎng)景對(duì)應(yīng)的測(cè)試方法中重復(fù)地編寫幾乎完全相同的代碼來初始化Employee類的對(duì)象。
這樣的單元測(cè)試模式在企業(yè)級(jí)應(yīng)用開發(fā)的場(chǎng)景中比比皆是。開發(fā)者經(jīng)常很容易在測(cè)試第二個(gè)場(chǎng)景時(shí),順手從第一個(gè)場(chǎng)景的單元測(cè)試中復(fù)制初始化數(shù)據(jù)模型的代碼,略作修改來描述第二個(gè)測(cè)試場(chǎng)景,后面的測(cè)試場(chǎng)景也如法炮制。這樣顯然會(huì)造成測(cè)試代碼中存在大量的模板代碼(Boilerplate code),進(jìn)一步降低了代碼的易讀性。
通常在開發(fā)項(xiàng)目的實(shí)踐中會(huì)引入構(gòu)建者模式(Builder Pattern)或者 Object Mother 組件來消除這些模板代碼。本文非常欣賞這些解決方案,下文會(huì)在此基礎(chǔ)上做進(jìn)一步討論。
初始化數(shù)據(jù)模型代碼膨脹
另外需要注意的是,前文舉例的代碼中為節(jié)省篇幅已經(jīng)做了很多簡(jiǎn)化。我們不僅用省略號(hào)折疊了(1)處之后可能傳入構(gòu)造方法的更多的初始化參數(shù),還折疊了在(b)處初始化 List
當(dāng)然在實(shí)踐中,經(jīng)常使用的策略是將大量無關(guān)的屬性設(shè)置成 null 或者空集合,但是這有時(shí)候會(huì)在被測(cè)組件對(duì)數(shù)據(jù)類有效性檢查中被攔截。特別是在某些演進(jìn)了一段時(shí)間的代碼庫(kù)中,我們經(jīng)常會(huì)遇到的困難是,由于在測(cè)試中構(gòu)造數(shù)據(jù)時(shí)采用了過多的 null 和空集合,一個(gè)新添加的數(shù)據(jù)有效性檢查步驟或者切面(AOP),會(huì)造成幾百個(gè)單元測(cè)試的失敗。逐一修復(fù)這些失敗的單元測(cè)試的工作量無疑是巨大的,同時(shí)是充滿風(fēng)險(xiǎn)的,因?yàn)榇藭r(shí)對(duì)單元測(cè)試的修改完全是為了兼容一個(gè)新添加的切面,而脫離了單元測(cè)試本身的業(yè)務(wù)上下文。
在這種情況下,開發(fā)者會(huì)越來越多選擇將相似的數(shù)據(jù)有效性檢查步驟散布在具體的業(yè)務(wù)代碼中,而非在構(gòu)造方法中統(tǒng)一檢查、或者通過切面集中實(shí)現(xiàn)??梢?,單元測(cè)試的不良設(shè)計(jì),會(huì)反過來增加生產(chǎn)代碼的維護(hù)難度,拖累了生產(chǎn)代碼的演進(jìn)。
EasyModeling提供的能力
造成開發(fā)者寫出類似單元測(cè)試的原因是廣泛存在的。例如,Employee 類沒有提供更靈活的構(gòu)造方法,也沒有 Builder 模式的構(gòu)造器。從 Employee 類自身的職責(zé)的角度出發(fā),它的確沒有理由提供一個(gè)僅包含 LocalDate dateOfJoining 作為參數(shù)的構(gòu)造方法。在很多業(yè)務(wù)場(chǎng)景下,數(shù)據(jù)模型類也完全有可能就是不允許通過 Builder 模式來構(gòu)造的。我們當(dāng)然不能為了編寫測(cè)試代碼的便利,而去修改生產(chǎn)實(shí)現(xiàn)代碼。又例如,代碼中可能存在對(duì) Employee 類的數(shù)據(jù)合法性校驗(yàn)。這些校驗(yàn)可能是類似切面的形式存在的,導(dǎo)致我們無法方便地在單元測(cè)試中忽略它。
在實(shí)際項(xiàng)目中,開發(fā)者很容易從“消除重復(fù)”的角度,抽象出相應(yīng)的工廠類來提供測(cè)試所需要的數(shù)據(jù)模型實(shí)例。Martin Fowler 也在他的博客的短文 Object Mother 中簡(jiǎn)要討論了相關(guān)的思路。但是在測(cè)試中使用工廠組件雖然消除了很多重復(fù)代碼,卻沒有提供針對(duì)不同的測(cè)試場(chǎng)景的靈活定制能力,因此一些項(xiàng)目又會(huì)同時(shí)采用 Builder 模式來提供定制能力。我自己在多個(gè)項(xiàng)目上引入 Object Mother 來提供測(cè)試數(shù)據(jù)實(shí)例后發(fā)現(xiàn),這些工廠類本身又具有非常固定的代碼模板,于是我開始考慮開發(fā)一個(gè)工具來自動(dòng)生成這種工廠類。
受到 Builder 模式和 Object Mother 思想的啟發(fā),我開發(fā)了 EasyModeling 來嘗試簡(jiǎn)化 Java 單元測(cè)試的編寫,并提高測(cè)試的可讀性和易維護(hù)性。EasyModeling 是一個(gè) Java 注解處理器庫(kù),它主要提供三個(gè)方面的功能:
- EasyModeling在編譯期根據(jù)指定的數(shù)據(jù)模型類的結(jié)構(gòu),生成對(duì)應(yīng)的數(shù)據(jù)模型工廠類,以方便單元測(cè)試快速生成數(shù)據(jù)模型類的實(shí)例。通過向 EasyModeling 注冊(cè)一個(gè)數(shù)據(jù)模型類,單元測(cè)試的編寫者只需要調(diào)用 EasyModeling 所提供工廠類的靜態(tài)方法,就可以立即得到這個(gè)數(shù)據(jù)模型類的實(shí)例。
- EasyModeling 還可以在單元測(cè)試的運(yùn)行時(shí),自動(dòng)初始化它所生成的數(shù)據(jù)模型實(shí)例。在生成數(shù)據(jù)模型實(shí)例時(shí),EasyModeling 默認(rèn)的行為是給數(shù)據(jù)模型實(shí)例的字段填充隨機(jī)值,讓開發(fā)者不需要再耗費(fèi)精力去填充對(duì)測(cè)試場(chǎng)景無意義的屬性。同時(shí),開發(fā)者仍然有機(jī)會(huì)向 EasyModeling 指定每個(gè)數(shù)據(jù)模型類的每個(gè)字段所需的初始化方式。
- 另外,EasyModeling 還在其生成的工廠類中提供了一個(gè) Builder 模式的構(gòu)建器。利用這個(gè)構(gòu)建器,開發(fā)者可以定制、并僅定制與當(dāng)前測(cè)試場(chǎng)景相關(guān)的字段,使單元測(cè)試簡(jiǎn)短、清晰、易讀。
在編碼層面,EasyModeling 的行為完全發(fā)生在測(cè)試包中,絲毫不會(huì)侵入項(xiàng)目的生產(chǎn)實(shí)現(xiàn)代碼。同時(shí),EasyModeling 只會(huì)照顧開發(fā)者向它注冊(cè)的數(shù)據(jù)類型類,而不會(huì)在代碼庫(kù)中主動(dòng)搜索。所以即使是維護(hù)已久的代碼庫(kù),從任何時(shí)間點(diǎn)引入 EasyModeling 都不會(huì)造成額外的負(fù)擔(dān)。
EasyModeling簡(jiǎn)化后的單元測(cè)試
在引入了 EasyModeling 后,本文中第一節(jié)中的單元測(cè)試?yán)涌梢缘玫斤@著地簡(jiǎn)化:
圖片
除此之外,如前文提到,開發(fā)者需要在測(cè)試代碼中向 EasyModeling 注冊(cè) Employee 類:
圖片
首先我們看到,在引入 EasyModeling 后,單元測(cè)試的代碼在篇幅上得到了非常明顯地簡(jiǎn)化。在單元測(cè)試中 (4) 處,EmployeeModeler 類就是由 EasyModeling 在編譯期生成的工廠類,通過引用 EmployeeModeler 類中的靜態(tài)方法 builder(),我們可以得到 Employee 類的Builder 的實(shí)例。請(qǐng)注意,此處使用的 Builder 類不是由 Employee 類自己編寫的,也不是通過如 Lombok 這樣的工具來提供的,而是由 EasyModeling 在其生成的工廠類 EmployeeModeler 來提供的。這樣的好處是,為了測(cè)試而準(zhǔn)備的 Builder 完全沒有侵入生產(chǎn)代碼。
其次,在 (4) 處生成的 Builder 類的實(shí)例中,EasyModeling 已經(jīng)為我們盡可能多地填充了所有的成員變量。因此,我們接下來只需要聚焦在當(dāng)前測(cè)試場(chǎng)景所關(guān)心的成員變量上。例如在 (5) 處,我們將 dateOfJoining 字段的內(nèi)容設(shè)置為指定的日期。在可讀性方面,由于避免了冗長(zhǎng)的初始化參數(shù),所以使開發(fā)者在閱讀單元測(cè)試時(shí),能夠快速理解測(cè)試場(chǎng)景,進(jìn)而也比較容易修改或維護(hù)單元測(cè)試。
第三,EasyModeling 在填充數(shù)據(jù)模型實(shí)例的屬性時(shí),不僅能夠填充一些 Java 應(yīng)用中常用的數(shù)據(jù)類型,包括基本類型、數(shù)組、集合、時(shí)間日期等等,還能夠進(jìn)一步填充當(dāng)前數(shù)據(jù)模型所引用的其他數(shù)據(jù)模型。例如 Employee 類中引用的 List
最后,為了讓 EasyModeling 幫我們生成 Employee 類的工廠類,如以上代碼中 (6) 處,開發(fā)者只需要在任意的一個(gè)類上通過 @Model 注解聲明即可。EasyModeling在編譯期為所有被 @Model 注解聲明的數(shù)據(jù)模型類生成對(duì)應(yīng)的工廠(Modeler)類。
除此之外,EasyModeling 還提供了其他一些好用的特性,限于篇幅,具體的用法請(qǐng)參考文檔。
EasyModeling的不足和未來
但是由于我的業(yè)余精力和能力都非常有限,EasyModeling 目前還處于它成長(zhǎng)的初期,存在幾點(diǎn)顯然的不足。
第一,沒有維護(hù)良好的使用文檔。目前我只維護(hù)了一份項(xiàng)目 Readme 文件,作為簡(jiǎn)要的使用文檔,導(dǎo)致一些略高級(jí)的使用方法和一些從新版本開始支持的功能并沒有體現(xiàn)在文檔中。
第二,沒有維護(hù)文檔注釋。遵循代碼整潔的原則,在長(zhǎng)期從事的企業(yè)應(yīng)用開發(fā)中,我?guī)缀醪粫?huì)寫任何形式的注釋。所以我也沒有意識(shí)到,在維護(hù)一個(gè)更偏底層的開源工具庫(kù)時(shí),充分的文檔注釋是非常必要的。一方面,文檔注釋便于開發(fā)者用戶查看閱讀,也便于有興趣的貢獻(xiàn)者參與開發(fā)。另一方面,由于這種較為基層的工具中無可避免地要使用一些魔法,如果沒有良好的注釋,隨著時(shí)間推移,可能連我自己也會(huì)忘記其中的細(xì)節(jié)。
由于 EasyModeling 是一個(gè)關(guān)注單元測(cè)試的工具,而不會(huì)入侵任何生產(chǎn)代碼,因此,在 Java 項(xiàng)目中引入 EasyModeling 幾乎不會(huì)對(duì)項(xiàng)目的可靠性、安全性造成任何風(fēng)險(xiǎn)。所以如果你對(duì)這個(gè)工具感興趣,認(rèn)為它有可能幫助你提高編寫測(cè)試的效率,請(qǐng)不妨引入到你的項(xiàng)目中嘗試使用。
未來,由于我自己在項(xiàng)目上會(huì)持續(xù)使用 EasyModeling 來構(gòu)建測(cè)試數(shù)據(jù),所以我基本可以保證持續(xù)維護(hù)這個(gè)工具。在近期,我將聚焦在完善使用文檔,以及修復(fù)從用戶反饋的一些缺陷。在EasyModeling 的功能特性方面,雖然我手上目前依然積壓著一些我自己想要實(shí)現(xiàn)的功能,但是我更想從用戶的反饋中收集更多有趣的好主意,再來推進(jìn)下一階段的功能演進(jìn)。
當(dāng)前標(biāo)題:簡(jiǎn)化Java單元測(cè)試數(shù)據(jù)
網(wǎng)頁路徑:http://m.fisionsoft.com.cn/article/dpcjjoo.html


咨詢
建站咨詢
