單元測試核心要求:快速、獨立、可重複。但現實中,被測單元常依賴資料庫、遠端 EJB、Web Service、靜態工具類、單例、JNDI 查找等「難以控制的外部依賴」。傳統 Mock 物件依賴介面注入 (Constructor/Setter Injection),無法處理:
AspectJ 以「切面導向程式設計 (AOP)」橫切能力,在不修改業務代碼前提下,於編譯/載入時織入測試專用行為,徹底解決上述困境。
// ClientBean - 依賴遠端 EJB
public class ClientBean {
private CustomerManager manager;
public ClientBean() throws Exception {
InitialContext ctx = new InitialContext();
Object obj = ctx.lookup("java:comp/env/ejb/CustomerManager");
CustomerManagerHome home = (CustomerManagerHome) obj;
manager = home.create(); // 遠端呼叫
}
public String[] getCustomers(int age) throws Exception {
return manager.getCustomersOver(age);
}
public boolean register(String name) {
try { manager.register(name); return true; }
catch (Exception e) { return false; }
}
}
測試痛點:需啟動應用伺服器、部署 EJB、準備測試資料、網路通訊——慢、脆、難隔離。
public aspect ObjectReplacement {
// 定義測試方法切點
pointcut inTest() : execution(public void ClientBeanTest.*());
// 選擇測試流程中的 JNDI 查找
pointcut jndiLookup(String name) :
cflow(inTest()) &&
call(Object Context.lookup(String)) &&
args(name);
// 環繞建議:攔截查找,返回 Mock Home
Object around(String name) : jndiLookup(name) {
if ("java:comp/env/ejb/CustomerManager".equals(name)) {
return new MockCustomerManagerHome();
}
throw new Error("Unexpected JNDI lookup: " + name);
}
}
public class MockCustomerManagerHome implements CustomerManagerHome {
public CustomerManager create() throws RemoteException, CreateException {
return new MockCustomerManager();
}
// 其他 EJBHome 方法拋出 UnsupportedOperationException
}
public class MockCustomerManager implements CustomerManager {
public String[] getCustomersOver(int years) {
String[] customers = new String[55];
for (int i = 0; i < 55; i++) customers[i] = "Customer " + i;
return customers;
}
public void register(String name) throws NameExistsException {
if (!name.equals(ClientBeanTest.NEW_CUSTOMER))
throw new NameExistsException(name + " exists!");
}
}
適用於「已部署整合環境,但需屏蔽特定外部呼叫」場景 (如 Cactus 伺服器端測試)。
public aspect CallReplacement {
pointcut inTest() : execution(public void ClientBeanTest.test*());
pointcut callToRegister(String name) :
cflow(inTest()) &&
call(void CustomerManager.register(String)) &&
args(name);
pointcut callToGetCustomersOver() :
cflow(inTest()) &&
call(String[] CustomerManager.getCustomersOver(int));
// 直接替換方法邏輯,無需 Mock 類
void around(String name) throws NameExistsException : callToRegister(name) {
if (!name.equals(ClientBeanTest.NEW_CUSTOMER))
throw new NameExistsException(name + " exists!");
}
String[] around() : callToGetCustomersOver() {
String[] customers = new String[55];
for (int i = 0; i < 55; i++) customers[i] = "Customer " + i;
return customers;
}
}
優勢:測試代碼更精簡,無需單獨 Mock 類/存根。
<target name="objectReplacement">
<antcall target="compileAndRunTests">
<param name="argfile" value="${src}/ajtest/objectReplacement.lst"/>
</antcall>
</target>
<target name="callReplacement">
<antcall target="deployAndRunTests">
<param name="argfile" value="${src}/ajtest/callReplacement.lst"/>
</antcall>
</target>
@base.lst
MockCustomerManagerHome.java
MockCustomerManager.java
ObjectReplacement.aj
@base.lst
CallReplacement.aj
RunOnServer.aj // Cactus 整合
單純切換 `argfile` 即可在「純單元測試」與「伺服器端整合測試」間切換,業務代碼零修改。
public aspect RunOnServer {
declare parents: ClientBeanTest extends ServletTestCase;
}
一行切面宣告,將繼承 `TestCase` 的 JUnit 測試類「升級」為繼承 `ServletTestCase` 的 Cactus 測試類,自動獲得伺服器端執行能力。編譯時織入,生產部署零痕跡。
| 傳統 Mock 難點 | AspectJ 解法 |
|---|---|
| 靜態方法/構造函數攔截 | call() 切點攔截任意方法調用/構造 |
| JNDI/全域資源查找替換 | cflow() 限定測試流程 + 環繞建議返回 Mock |
| 第三方代碼無法注入 | 無需修改源碼,編譯/載入時織入 |
| 測試配置切換困難 | Ant argfile 切換切面集合,零代碼變更 |
| 測試代碼污染生產構建 | 編譯時織入,發布構建排除測試切面 |
AspectJ 不只是 AOP 框架,更是「測試時的時光機」——它讓我們在不改變過去 (業務代碼) 的前提下,為測試當下創造理想的隔離環境。結合 Mock 物件,AspectJ 將「難以測試」轉化為「精確可控」,讓單元測試真正回歸「快、獨立、可重複」的本質。對於擁抱 XP/敏捷、追求高測試覆蓋率的團隊,掌握 AspectJ 測試切面技術是邁向測試成熟度的關鍵里程碑。