剛學了 Annotation,手實在很癢,就先拿 EasyMock 2.0 來開刀吧!
先來看看一般利用 EasyMock 測試長什麼樣子:
public MyServiceImplTest extends TestCase {
private IMocksControl control ;
private FirstDAO mockFirstDAO ;
private SecondDAO mockSecondDAO ;
private MyServiceImpl service ;
public void setUp() {
//this is object under test.
service = new MyServiceImpl() ;
//prepare mock and control
control = EasyMock.createStrictControl() ;
mockFirstDAO = control.createMock(FirstDAO.class) ;
mockSecondDAO = control.createMock(SecondDAO.class) ;
//inject mock into service via setter injection:
service.setFirstDAO(mockFirstDAO);
service.setSecondDAO(mockSecondDAO);
}
}
OK,這片段的程式是在替測試作準備,先建立要測試的物件 "service",然後再利用 EasyMock 建立配合測試用的合作物件,就是上面那兩個 mock DAO。最後再將作好的 mock DAO inject 到 service 裡。這樣,後面的 testMethod 便可以開始作 service 的 unit test。這個 test fixture 雖短,但是寫多了也是很煩地... 老是在那裡 createMock()/setXXX()... 我想可以利用 Annotation 簡化成這樣:
public MyServiceImplTest extends TestCase {
private IMocksControl control ;
@CreateMock
private FirstDAO mockFirstDAO ;
@CreateMock
private SecondDAO mockSecondDAO ;
@InjectMock
private MyServiceImpl service ;
public void setup() {
//this is object under test.
service = new MyServiceImpl() ;
//instantiate mock and then inject into object under test
control = EasyMockHelper.setup(this) ;
}
}
EasyMockHelper.setup(this) 會替這個 testCase 的實例作 reflection,找出有 annotate 的 field 然後建立 mock 或是 inject mock:
public abstract class EasyMockHelper {
//宣告 Annotation @CreateMock
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreateMock {}
//宣告 Annotation @InjectMock
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectMock {}
public static IMocksControl setup(TestCase testCase) {
IMocksControl control = EasyMock.createStrictControl();
Field[] fields = testCase.getClass().getDeclaredFields();
for (Field field : fields) {
//如果 field 有 @CreateMock, 就建立 mock.
if (field.isAnnotationPresent(CreateMock.class)) {
Object mock = createMockForField(testCase, control, field);
injectMockIfRequired(testCase, mock);
}
}
return control;
}
private static Object createMockForField(TestCase testCase,
IMocksControl control, Field field) {
Object mock = control.createMock(field.getType());
try {
field.setAccessible(true);
field.set(testCase, mock);
} catch (IllegalAccessException e) {
throw new IllegalStateException("can not access field:" + field, e);
}
return mock;
}
private static void injectMockIfRequired(TestCase testCase, Field[] fields) {
//省略
}
}
EasyMockHelper 裡面建立了兩個 inner annotation,一個是 @CreateMock,另一個是 @InjectMock。這兩個都沒有含有其他設定值,所以算是 Maker Annotation 的一種。設定上,第一,我們設定它們只能用在 field 的位置: @Target(ElementType.FIELD)。第二,因為我們在執行時期才會去讀這些 Annotation,所以要將它們保留至 runtime: @Retention(RetentionPolicy.RUNTIME)。
而 setup(testCase) method 裡面則利用 reflection -- isAnnotationPresent(CreateMock.class) 找出所有有 annotate @CreateMock 的field,然後再替它建立 mock 物件。後半段 injectMockIfRequired(...) 則是 reflect @InjectMock 來作 setXXX(...) 的動作,這就省略不提了,因為跟 @CreateMock 差不多
另一種設計方式是將兩個 annotation 合一,改成像這樣:@EasyMock("create") / @EasyMock("inject"),這個小工具倒是用不著這麼省... 用兩個 Maker 簡單多囉。
過去作 reflection 頂多只能靠 interface 或是 "magic" 名稱來做,現在 JDK 5 多了 Annotation,可以少掉不少工夫啦。果然是個方便的工具啊~~~
話說回來,如果 Annotation 一大起來也不是好玩地,看看這個吧:EJB3 annotation cheat sheet... orz