10 August 2005

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


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>