剛學了 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