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

Thursday, November 15, 2007

Generic DAO (2) 基本的 Generic DAO Implementation

上一篇提到的 Generic DAO 的效果, 旨在讓使用者不需要再重新 implement 每一個 DAO 大同小異的部份 : create, update, delete etc. 就連 finder methods, 也只要寫好 HQL 就好了, 不必再重覆每個finder 去 getSession(), createQuery() 等等.

這個 framework 的第一步, 就是 GenericDao(從原文抄來借用一下 :P ).

Generic DAO 大概的樣子就是
public interface GenericDao<T, PK extends Serializable> {
_ PK create(T newInstance);
_ T read(PK id);
_ void update(T transientObject);
_ void delete(T persistentObject);
}

在 Generic DAO 本身就有提供最基本的CRUD operation

public class GenericDaoHibernateImpl <T, PK extends Serializable>
___ implements GenericDao<T, PK>, FinderExecutor {
___ private Class<T> type;

___ public GenericDaoHibernateImpl(Class<T> type) {
_______ this.type = type;
___ }

___ public PK create(T o) {
_______ return (PK) getSession().save(o);
___ }

___ public T read(PK id) {
_______ return (T) getSession().get(type, id);
___ }

___ public void update(T o) {
_______ getSession().update(o);
___ }

___ public void delete(T o) {
_______ getSession().delete(o);
___ }

___ // Not showing implementations of getSession() and setSessionFactory()
}


(原本的 implementation 是在 Spring Config 中是在Spring config inject Hibernate 的 SessionFactory. 但我則建議用 JPA 的 EntityManager, 並利用 @PersistenceContext 讓 Spring 自動 inject EntityManager. 詳細做法之後會解釋.)

好了, 有了這個 Generic DAO implementation, 但怎樣才能沒有 implementation 也能跑? 這就要借助 Spring 的 ProxyFactoryBean. 利用 ProxyFactoryBean, Spring 會為我提供的 interfaces 自動 Generate 一個 implementation class. 比對前一篇的 Spring

<bean id="abstractDaoTarget"
_______ class="foo.GenericDaoHibernateImpl"
_______ abstract="true" />

<bean id="staffDao" class="org.springframework.aop.framework.ProxyFactoryBean">
_ <property name="proxyInterfaces" value="foo.StaffDao,foo.FinderExecutor">
_ <property name="target">
___ <bean parent="abstractDaoTarget">
_____ <constructor-arg>
_______ <value>foo.Staff</value>
_____ </constructor-arg>
___ </bean>
_ </property>
</bean>



簡單的解釋, 上面的 config 會叫 Spring 生成一個 GenericDaoHibernateImpl, 然後再生一個 Proxy Bean 來包著之前的Generic DAO Impl, 此 Proxy 亦會額外 implement StaffDao 和 FinderExecutor 的 interface. 但問題是, StaffDao 和 FinderExecutor 的 method, 這個 Proxy Bean 怎樣 implement? 其實 Proxy Factory Bean 不會做任何有意義的 implementation. 這個 Dummy 的 object instance 旨在讓 Spring AOP 的 Interceptor 攔截再跑自己想要的 logic. 那些自動 generate 出來的沒用 method implementation 是不會用到的.

原本的 JGenericDao 做法順便把 Interceptor 塞給ProxyFactoryBean. 但這做法在 Spring 2.0.x, 如果同時有用到新的 AOP config 就會不正常運作, 所以我利用了 AOP 的 xml config 來提供 interceptor

<bean id="queryInterceptor"
___ class="foo.FinderIntroductionInterceptor" />

<aop:config>
___ <aop:aspect ref="queryInterceptor">
_______ <aop:pointcut id="findQuery"
___________ expression="execution(* foo..*Dao.find*(..)) and this(foo.dao.finder.FinderExecutor)" />
_______ <aop:around pointcut-ref="findQuery" method="invokeFind" />
___ </aop:aspect>

</aop:config>



這樣每逢跑 DAO 中以 find 開首的 methods, 我們的 queryInterceptor 就會攔截.

攔截了要做什麼? 借用 "Don't repeat the DAO!" 的 code, 就是攔截後, 不直接 invoke 原本的 method, 轉而 invoke target class 的 "executeFinder" method.

public class FinderIntroductionInterceptor implements IntroductionInterceptor {

___ public Object invokeFind(MethodInvocation methodInvocation) throws Throwable {

_______ FinderExecutor genericDao = (FinderExecutor) methodInvocation.getThis();

_______ String methodName = methodInvocation.getMethod().getName();
_______ if (methodName.startsWith("find")) {
___________ Object[] arguments = methodInvocation.getArguments();
___________ return genericDao.executeFinder(methodInvocation.getMethod(), arguments);
_______ } else {
___________ return methodInvocation.proceed();
_______ }
___ }

___ public boolean implementsInterface(Class intf) {
_______ return intf.isInterface() && FinderExecutor.class.isAssignableFrom(intf);
___ }
}


Generic DAO 的 executeFinder 做的, 則是基於 method name, 去找 named query 來 invoke. 如果 method 有 parameter pass in 的話, 就順便 bind 到 query.
public List<T> executeFinder(Method method, final Object[] queryArgs) {
____ final String queryName = queryNameFromMethod(method);
____ final Query namedQuery = getSession().getNamedQuery(queryName);
____ String[] namedParameters = namedQuery.getNamedParameters();
____ for(int i = 0; i < queryArgs.length; i++) {
____________ Object arg = queryArgs[i];
____________ Type argType = namedQuery.setParameter(i, arg);
_____ }
_____ return (List<T>) namedQuery.list();
}

public String queryNameFromMethod(Method finderMethod) {
____ return type.getSimpleName() + "." + finderMethod.getName();
}



這個便是 Generic DAO 最基本的功能.

但這個 Generic DAO 有很多欠缺的功能, 下回就是我自己基於這個 framework 作的新功能. 當中包括 annotated parameters, provided Query, annonatation-based QBC and QBE, 和 self-implemented finders.

Wednesday, November 14, 2007

Generic DAO (1)

反正丟空了這麼久, 就用這個 blog 來做學習記錄吧.

在公司最近搞的 application framework 利用了 Hibernate, 在Don't Repeat DAO 這文章中找到一個 Generic DAO 的架構, 加上了自己的 extensions, 在這裡 share 想法.

顧名思義, 這個 Pattern 是希望不要再重覆又重覆的去建一大堆大同小異的 DAO. 其實每個 DAO, 主要都不外乎是 create, delete, update 和一堆 finder method. 這個 Generic DAO 的做法. 大概就是以 Hibernate 去處理掉 CRUD 中的 Create, Update, Delete 和最基本的 Read, 然後以最簡單的方法去讓 developer 去 define 其他 finder methods. 而define finder method, 也讓 developer 只要集中於 query 上, 而不用再重覆寫一些 getSession(), session.createQuery(), return query.list() 之類的東西.

當中用到的主要 technology 有 Spring (AOP 的 Interceptor 和 ProxyFactoryBean) 和 Hibernate (當然)。

先說說基本效果.
假設我寫了一個 Staff class, 然後要做 DAO. 靠這個 framework, 我要做的是:

1. 在 Staff class 中加上 OR Mapping. 我選擇了用 annotation.

@Entity
@Table(name="STAFF")
public class Staff {
_ @Id
_ @GeneratedValue(strategy=GenerationType.AUTO)
_ @Column(name="ID")
_ private Long id;

_ @Column(name="NAME")
_ private String name;

_ @Column(name="SALARY")
_ private BigDecimal salary;

_ // getters and setters and other methods
}


2. 寫 DAO 的 Interface

public interface StaffDao extends GenericDao<Staff, Long> {
}



3. 在 Spring Config 加上

<bean id="abstractDaoTarget" class="foo.GenericDaoHibernateJpaImpl" abstract="true" />

<!-- Dao Layer instances -->
<bean id="staffDao" class="org.springframework.aop.framework.ProxyFactoryBean">
_ <property name="proxyInterfaces" value="foo.StaffDao">
_ <property name="target">
___ <bean parent="abstractDaoTarget">
_____ <constructor-arg>
_______ <value>foo.Staff</value>
_____ </constructor-arg>
___ </bean>
_ </property>
</bean>
和在 persistence.xml 加上 Staff class 的名字

4. DAO 就可以用了. 對, 沒有任何 implementation class, 就已經有一個 提供基本四個 CRUD operation 的 DAO 可以用了. 或者你會說, 這個 DAO 沒有什麼用, 最重要的 finder method 都沒有, 我要用 Staff name search Staff 怎麼做? 好的, 要加 finder methods, 只要:

5. 修改一下 StaffDao, 加進你想要的 finder method
public interface StaffDao extends GenericDao<Staff, Long> {
_ public void List<Staff> findStaffByName(String name);
}

6. 在 Staff class 前面加上

@NamedQueries({
_ @NamedQuery(name="Staff.findStaffByName",
______ query="from Staff s where s.name= :name")
})

一樣毋需任何 implementation, 你的 DAO 就已經提供了 finder.

怎樣達到這個效果, 下一篇會開始解釋. 文章開首的 link 提供了大概的做法.

(由於 Blogger.com 對於 source code 的顯示非常不方便, 一直吃掉我的 space, 所以大家就自己把 source code 前的 underscore 消去吧.)