然后,編寫測(cè)試用例以驗(yàn)證方面正確地與目標(biāo)交互,如清單 4 所示:
清單 4. 與 mock 目標(biāo)交互以測(cè)試建議
public void setUp() throws Exception {
super.setUp();
setUpMockHighlightUtil();
words = Collections.singleton("big");
mockTarget = new HighlightMockTarget();
mockTarget.setHighlightedWords(words);
}
//mock setup/tearDown omitted
public void testHighlighting() {
mockUtil.expects(once())
.method("highlight")
.with(eq("I am a big bear!"), eq(words))
.will(returnValue("highlighted text"));
String shouldBeHighlighted = mockTarget.getSomeString();
assertEquals(shouldBeHighlighted, "highlighted text");
}
注意在這個(gè)例子中,我結(jié)合了 mock 目標(biāo)和 mock 對(duì)象(如在 第 III 節(jié), 模式 2 中所描述的)。mock 目標(biāo)為下面三種技術(shù)提供了基礎(chǔ)。
模式 1. 通過擴(kuò)展一個(gè)抽象方面并提供一個(gè)切點(diǎn)來測(cè)試建議
針對(duì) :橫切功能
概述 :Prework :如果有必要,重新編寫方面,將它分為一個(gè)抽象方面以及 一個(gè)擴(kuò)展它并具體化一個(gè)或者多個(gè)切點(diǎn)的具體方面。
有了抽象方面后,在測(cè)試類中創(chuàng)建一個(gè) mock 目標(biāo)。創(chuàng)建一個(gè)擴(kuò)展了抽象方 面的測(cè)試方面。讓測(cè)試方面提供明確針對(duì) mock 目標(biāo)的切點(diǎn)。這個(gè)測(cè)試通過查找 建議的已知副作用或者使用一個(gè) mock 對(duì)象來驗(yàn)證方面中的建議是否成功。
示例:擴(kuò)展 AbstractHighlighter
假定已經(jīng)編寫了 上一節(jié)中的測(cè)試代碼。為了使測(cè)試通過,必須將 Highlighter 方面分解為一個(gè)抽象方面和一個(gè)子方面,如下所示:
public abstract aspect AbstractHighlighter {
public abstract pointcut highlightedTextProperties();
//... aspect continues
}
public aspect HighlightResults extends AbstractHighlighter {
public pointcut highlightedTextProperties() :
(
//...define pointcut as before
);
}
下一步,用一個(gè)只用于測(cè)試案例的方面擴(kuò)展 AbstractHighlighter 方面。下 面我將它展示為測(cè)試案例的一個(gè)靜態(tài)內(nèi)部方面:
private static aspect HighlightsTestClass extends AbstractHighlighter {
public pointcut highlightedTextProperties() :
execution(public String HighlightMockTarget.*(..));
}
這個(gè)方面通過選擇 mock 目標(biāo)上所有的方法執(zhí)行具體化了 highlightedTextProperties 切點(diǎn)。
優(yōu)缺點(diǎn)
顯然,這種測(cè)試過程是一種人造的情況。對(duì)一個(gè)假的對(duì)象測(cè)試假的方面。不 過,這只是表明測(cè)試的不是真正的切點(diǎn)。仍然可以驗(yàn)證建議和抽象方面所指定的 ITD 代碼。在例子中,測(cè)試驗(yàn)證建議正確地編組了來自 ITD 的數(shù)據(jù)以及原來聯(lián) 結(jié)點(diǎn)的返回值、將它傳遞給一個(gè)工具類并返回新的結(jié)果。這涉及了相當(dāng)多的行為 。使用一個(gè) mock 目標(biāo)還使測(cè)試更清晰了,因?yàn)闇y(cè)試的讀者不必閱讀真正目標(biāo)的 行為以及方面的行為。這種測(cè)試在為方面庫編寫單元測(cè)試時(shí)特別有用,因?yàn)橹挥?到了方面加入到具體的應(yīng)用程序中以后才會(huì)有真實(shí)的目標(biāo)。
如果將方面分解以利用這種模式的好處,那么您可能使它更具可擴(kuò)展性。比 如,如果系統(tǒng)的新部分需要參與突出顯示行為,那么它們可以擴(kuò)展抽象的方面并 定義覆蓋新情況的切點(diǎn)。這樣,抽象方面與它所建議的系統(tǒng)解耦了。
模式 2. 測(cè)試與 mock 目標(biāo)匹配的切點(diǎn)
針對(duì) :橫切規(guī)范和功能
概述 :這項(xiàng)技術(shù)與上一技術(shù)密切相關(guān)。這次不是擴(kuò)展一個(gè)抽象類,而是編寫 mock 目標(biāo),以使它匹配要測(cè)試的方面上的一個(gè)切點(diǎn)?梢酝ㄟ^檢查方面是否建 議了 mock 目標(biāo)來測(cè)試切點(diǎn)是否正確。如果要測(cè)試的切點(diǎn)過度專門化,那么可能 需要重新編寫它,使得 mock 目標(biāo)可以更容易地“預(yù)定”建議。
示例:基于一個(gè)標(biāo)志接口測(cè)試切點(diǎn)
不是使突出顯示方面成為抽象的,而是改寫切點(diǎn)使它匹配 Highlightable 接 口上的方法執(zhí)行:
public pointcut highlightedTextProperties() :
execution(public String Highlightable+.get*());
這種寬泛的切點(diǎn)匹配 Highlightable 上的所有 String getter。因?yàn)榍悬c(diǎn)不 枚舉特定的類,它已經(jīng)匹配了 mock 目標(biāo)上的 getSomeString() 方法。測(cè)試的 其余部分保持不變。
變化:使用一個(gè)注釋
還可以編寫切點(diǎn)以部分根據(jù) Java 5.0 元數(shù)據(jù)進(jìn)行匹配。例如,下面修改后 的切點(diǎn)匹配用 @Highlighted 注釋修飾的方法執(zhí)行:
public pointcut HighlightedTextProperties() :
execution(@Highlighted public String Highlightable+.*());
//you can apply the annotation in the source, or using the declare- annotation form
declare @method : public String SearchResult+.getTitle(..) : @Highlighted;
declare @method : public String SearchResult+.getProduct(..) : @Highlighted;
可以通過添加注釋到其 getSomeString() 方法,使 mock 目標(biāo)匹配新的切點(diǎn) :
@Highlighted
public String getSomeString() {
return "I am a big bear!";
}
優(yōu)缺點(diǎn)
這項(xiàng)技術(shù)還明確地分離了對(duì)方面行為與目標(biāo)應(yīng)用程序的行為的測(cè)試,使測(cè)試 變?yōu)楦?dú)立。如果切點(diǎn)還沒有編寫為容納 mock 目標(biāo),那么應(yīng)當(dāng)通過重新編寫它 們得到一個(gè)耦合更松散的方面。通過使方面足夠一般化,可以影響測(cè)試類中的 mock 目標(biāo),還會(huì)保證它可以容易地讓真實(shí)類參與方面的行為。
模式 3. 驗(yàn)證更復(fù)雜的切點(diǎn)(一個(gè)特殊情況)
針對(duì) :橫切規(guī)范和功能
概述 :上一個(gè) mock 目標(biāo)是簡(jiǎn)單的,但是也可以將 mock 目標(biāo)編寫為模擬復(fù) 雜的聯(lián)結(jié)點(diǎn)(如 cflow())或者要影響的一系列聯(lián)結(jié)點(diǎn)。
例子:模擬 cflow
假定希望對(duì)于下載的報(bào)告關(guān)閉突出顯示?梢约尤胍粋(gè) highlightExceptions切點(diǎn)以排除由 ReportGenerator 調(diào)用的任何 getter,如 下所示:
public pointcut highlightedTextProperties() :
execution(public String Highlightable+.get*())
&& !highlightExceptions();
public pointcut highlightExceptions() :
cflow(execution(* ReportGenerator+.*(..)));
然后可以編寫一個(gè) mock ReportGenerator,它調(diào)用 HighlightMockTarget 以測(cè)試沒有進(jìn)行突出顯示:
private class MockGenerator implements ReportGenerator {
public void write(OutputStream stream) throws IOException {
mockTarget.getSomeString();
}
}
public void testNoHighlight() throws Exception {
mockUtil.expects(never()).method("highlight");
MockGenerator accessor = new MockGenerator();
accessor.write(null);
}
不過,可以想像為更復(fù)雜的匹配情況(例如,somePointcut() && ! cflowbelow(somePointcut()))創(chuàng)建一個(gè)類似的 mock 目標(biāo)?梢暬ぞ卟荒芙o 出關(guān)于使用運(yùn)行時(shí)檢查的切點(diǎn)(如 cflow())的匹配的詳細(xì)信息。用幾個(gè)代表性 的 mock 目標(biāo)檢查這種切點(diǎn)是值得的。
結(jié)束語
當(dāng)我看到未測(cè)試的代碼時(shí),覺得厭煩。沒有好的測(cè)試集的代碼通常有很多 問題,難于進(jìn)行有信任度的改變,并且難以重構(gòu)。不過,如果用方面實(shí)現(xiàn)橫切行 為,那么有了測(cè)試(并理解)應(yīng)用程序的橫切關(guān)注點(diǎn)的新方法。
測(cè)試方面與測(cè)試對(duì)象很相似。這兩種測(cè)試都需要將行為分解為可以單獨(dú)測(cè)試 的組件。一個(gè)要掌握的關(guān)鍵概念是橫切關(guān)注點(diǎn)分為兩個(gè)區(qū)域。首先是橫切規(guī)范, 它要回答的是關(guān)注點(diǎn)影響的是程序的哪些部分。其次是功能,它回答的是這些點(diǎn) 上會(huì)發(fā)生什么。如果只使用對(duì)象,那么這兩個(gè)區(qū)域是交叉的,因?yàn)殛P(guān)注點(diǎn)在應(yīng)用 程序中是糾纏在一起的。不過,使用了方面后,可以以一個(gè)領(lǐng)域?yàn)槟繕?biāo)或者同時(shí) 分別以兩個(gè)領(lǐng)域?yàn)槟繕?biāo)。
將方面編寫為可測(cè)試的,得到的設(shè)計(jì)好處與通過重構(gòu)面向?qū)ο蟮拇a來實(shí)現(xiàn) 可測(cè)試性所得到的好處相似。例如,如果將建議的正文轉(zhuǎn)移到一個(gè)可獨(dú)立測(cè)試的 類中,那么可以分析其行為而不用理解它橫切應(yīng)用程序的方式。如果修改切點(diǎn) 以使它們更能被 mock 目標(biāo)訪問,也使它們更可被系統(tǒng)中的非測(cè)試部分訪問。 不管是哪種情況,都提高了系統(tǒng)整體的靈活性和可插入性。
不久之前,我聽到了一個(gè)流傳的說法,說面向方面的程序不能測(cè)試。盡管這 個(gè)謠傳基本上已經(jīng)消失,我仍然認(rèn)為它是一個(gè)挑戰(zhàn)。我希望本文表明不僅可以對(duì) 方面進(jìn)行測(cè)試,而且在測(cè)試橫切時(shí),使用了方面后會(huì)好得多。