Friday, December 14, 2007

SevenMock 介紹

在做 Unit test 的人, 大概都知道要利用 Mock Object 來避開 dependencies, 但 Mock 這東西, 自己寫又很麻煩, test case 裡面每一個 test, Mock 要的反應也不太一樣, 每個 test 寫一個 Mock 的話, 既辛苦又難看. 所以才有各大 Mock Framework, 幫忙你能輕易的建立因應每一個 test 的 mock object.

出名的有 jMock, EasyMock. 但用過後, jMock 固然看得頭昏也看不明白搞什麼, EasyMock 用起來是簡單一點, 但對於 expected data 的檢查也是十分麻煩, 尤其是我只想檢查 expected input 的某些 attributes, EasyMock 做起來十分痛苦.

後來找到一個叫 SevenMock 的 mock framework, 用起來十分簡單, expected input 的 validation 的可讀性更是非常好.

下面是一個使用例子. 假設我有一個 TradeService的 實作要測:
public class TradeServiceImpl {
private tradeDao tradeDao;
private fundService fundService;

public void setTradeDao(TradeDao tradeDao) {
this.tradeDao = tradeDao;
}

public void setFundService(FundService fundService) {
this.fundService = fundService;
}

public boolean inputTrade(Trade trade) {
if (this.fundService.isEnoughBalance(trade.getPrice().mulplity(trade.getQty()))) {
this.fundService.deductBalance(trade.getPrice().mulplity(trade.getQty()));
trade.setStatus(Trade.Status.NEW);
this.tradeDao.createTrade(trade);
return true;
)
return false;
}
}

下面便是利用 SevenMock 的測試

public class TradeServiceImplTest {
@Test
public void testFoo() {
// create test target and mocks
TradeService tradeService = new TradeServiceImpl();
MockController mockControl = new MockController();
TradeDao tradeDao = (TradeDao)mockControl.getMock(TradeDao.class);
FundService fundService= (FundService)mockControl.getMock(FundService.class);

// Inject Mock objects to class-to-be-tested
tradeService.setTradeDao(tradeDao);
tradeService.setFundService(fundService);

// Set Expected Results
mockControl.expect(new FundServiceMockImpl() {
public boolean isEnoughBalance(BigDecimal amount){
// check if incoming value is correct
assertEquals(BigDecimal.valueOf(1000*400), amount);

// preset behavior: return true
return true;
}
});

mockControl.expect(new FundServiceMockImpl() {
public void deductBalance(BigDecimal amount){
// check if incoming value is correct
assertEquals(BigDecimal.valueOf(1000*400), amount);
}
});


mockControl.expect(new TradeDaoMockImpl() {
public void createTrade(Trade trade) {
assertEquals(Trade.Status.NEW,trade.getStatus());
return result;
}
});

// Finish setting expected behavior, then run test now
Trade myTrade = new Trade();
myTrade.setPrice(BigDecimal.valueOf(1000));
myTrade.setQty(BigDecimal.valueOf(400));
myTrade.setStatus(Trade.Status.NONE);

boolean result = tradeService.inputTrade(myTrade);

// verify results
assertTrue(result);
assertEquals(Trade.Status.NEW, myTrade.getStatus());

// Mock control will verify if there is missing step in invocation
mockControl.verify();
}
}
// dummy implementation class of mocked interfaces for use of SevenMock
class TradeDaoMockImpl implements TradeDao {
// all empty methods....
}

class FundServiceMockImpl implements FundService {
// all empty methods....
}



用起來十分直觀容易明白. 其中一個缺點是要 mock 的 interface, 需要一個 implementation class 來 override 為 anonymous class. 我這裡是弄一個 empty 的 implementation (TradeDaoMockImpl 及 FundServiceMockImpl), 靠 IDE 要弄這麼一個 empty implementation 很容易. 但要是你本身就有 implementations, 只要constructor 沒有什麼特別的 initialization, 直接用你自己的 implementation 來用也無不可, 反正不會真的 invoke 到它裡面的 method.

誠意推介.

Tuesday, December 04, 2007

Spring 中取得 advised-this 的做法

原來 Advised 拿 target 這一招不是經常成功的 orz... 待我有時間再去改改做法 :(



假設我有一個 object (假設叫是 class Foo), 外面墊了幾層 aspect, 如果我在 Foo 裡面 invoke 任何自己的 method, 那麼該 invocation 是沒有被攔截的.

那麼怎樣才能令自己 invoke 自己的 method 也能被 aspect 攔截? 那麼可以嘗試 inject advised 的 instance 給 object 自己. 在 Spring + JDK5 的環境下, 寫了一個簡單的bean post processor + annotation 去達到這目的.

BeanPostProcessor
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class InjectBeanSelfProcessor implements BeanPostProcessor {
___ public Object postProcessAfterInitialization(Object bean, String beanName)
___________ throws BeansException {
_______ // 找出 Proxy 背後真正的 target
_______ Object target = bean;
_______ while (target instanceof Advised) {
___________ try {
_______________ target = ((Advised)bean).getTargetSource().getTarget();
___________ } catch (Exception e) {
_______________ target = null;
_______________ break;
___________ }
_______ }
_______ // 如果順利找到 target
_______ if (target != null) {
___________ Method[] methods = target.getClass().getMethods();
___________ for (Method m : methods) {
_______________ if (m.getAnnotation(BeanSelf.class) != null
_______________________ && m.getParameterTypes().length == 1
_______________________ && m.getParameterTypes()[0].isAssignableFrom(bean.getClass())) {
___________________ try {
_______________________ m.invoke(target, bean);
___________________ } catch (IllegalArgumentException e) {
___________________ } catch (IllegalAccessException e) {
___________________ } catch (InvocationTargetException e) {
___________________ }
_______________ }
___________ }
_______ }
_______ return bean;
___ }

___ public Object postProcessBeforeInitialization(Object bean, String beanName)
___________ throws BeansException {
_______ return bean;
___ }
}


BeanSelf 的 annotation 從略, 反正是一個 Retention at Runtime, annotate at Method 的 annotation.

自己的 class 要 invoke 自己method 時被 aspect 攔截, 便這樣做:

public class FooImpl implements Foo {
___ private Foo self;

___ // 加上 BeanSelf, 讓 Spring inject Advised-self 進來
___ @BeanSelf
___ public void setSelf(Foo self) {
_______ this.self = self;
___ }
___ protected Foo getSelf() {
_______ return this.self;
___ }

___ public void foo() {
_______ // 不要直接 invoke bar() 或 this.bar()
_______ getSelf().bar();
___ }
___ public void bar() {
_______ //....
___ }
}


當然不要忘了在 Spring 的 app context config 加上 InjectBeanSelfProcessor.

Note: 謹記原來可以利用 Advised 來取得真正 target.

Reference:
http://fyting.javaeye.com/blog/109236