Monthly Archives: November 2011

复习ibatis

Ibatis 相比于 spring jdbc template的好处:

1.把 column value <=> JavaBean这种映射放到XML里来做会比在java代码里来做方便(作者很谦虚地说这只是他个人的体会)

2.SQL里的parameter可以用#age#, #name#来表示,而不是问号; 这可以增强sql的可读性

3.可以方便地将ResultSet一步映射为组合对象(如类A里面关联了类B的实例作为属性),免于用java进行手工组装。这意味着你的数据层bean可以写成domain风格,而不是table model风格

4.可以把动态SQL的拼写放在XML里,维持一个相对整洁的外观。这个在多条件搜索时大有用武之地。

5.可以在XML里重用SQL片断,比在JAVA里重用要直观一些

==========================================

一些边角的东西:

1.“null value replacement”可以让你设置:在参数值为空时,用某个值来填充

2. 尽量用resultMap,而不是resultClass. 看这里
3. 支持复合主键

4. 动态SQL拼写除了支持<isNull>和<isNotNull>,还支持<isPropertyAvailable>(对付多态对象有效),<isEmpty>(对付未trimToNull的字符串有效)和它们的反义词

隔离,分层,专注

把核心服务隔离出来,做成独立的层次;并使它内聚于自身,而不必受到非核心的东西的干扰。它向外界提供简明的视图,并只按自己的特性和目标变化,不必屈从于其它东西的需要。

其他的东西,不再跟核心服务混杂在一起;它们只能通过依赖、组合核心服务,实现自己的目标。

代价是: 处处需要适配

Service层的单元测试:在测试方法单元性和所需Mock的次数上取得平衡

都说单元测试应该喵准单个方法,每个测试方法只测一小部分逻辑。

但另一方面,如果测试时总是要Mock,你就要想办法少Mock一些东西,因为Mock代码写起来是很累、很难看的

Service层的代码往往就是这种情况。看个例子:

你会怎么测下面这样的代码:

//仔细看下,这是一棵4层的判定树
public class Service {
 
    //为了层次清晰,没有使用驼峰式命名
    public boolean do_biz() {

        if (!is_biz_one_valid()) {
            return false;
        }
        return is_biz_two_valid();
    }

    boolean is_biz_two_valid() {
        if (!is_two_a_valid()) {
            return false;
        }

        return is_two_b_valid();
    }

    boolean is_two_b_valid() {
        if (!is_two_b_left_valid()) {
            return false;
        }

        return is_two_b_right_valid();
    }
   ... 

本着每个测试方法只测一小块逻辑的原则,你可能会这样写:

    @Test
    public void test_do_biz() {
        Service service = new Service();
        // do mocking
        expect_biz_one_valid(service);
        expect_biz_two_valid(service);

        assertTrue(service.do_biz());

    }

    @Test
    public void test_biz_two() {
        Service service = new Service();
        // do mocking
        expect_biz_two_a_valid(service);
        expect_biz_two_b_valid(service);

        assertTrue(service.is_biz_two_valid());
    }

    @Test
    public void test_biz_two_b_valid() {
        Service service = new Service();

        // do mocking
        expect_two_b_left_valid(service);
        expect_two_b_right_valid(service);

        assertTrue(service.is_two_b_valid());
    }
   ... //只写了正例

问题:

  1. 你写了6个mock方法: expect_biz_one_valid,expect_biz_two_valid …Mock太多了,搞的测试类比被测类还长…..

  2. 如果被测类是一棵判定树,那你差不多对每个中间层结点都写了测试; 但,真的需要对它们都进行测试吗? 别忘了,你只有一个public方法,你只想直接验证 y = f(one,two_a,two_b_left,two_b_right) 结果,并不愿意分别验证 two_b = f(two_b_left, two_b_right), two = f(two_a, two_b), y = f(one,two)

何不干脆这样写:




    @Test
    public void test_do_biz() {
        Service service = new Service();
        // do mocking
        expect_biz_one_valid(service);
        expect_biz_two_a_valid(service);
        expect_two_b_left_valid(service);
        expect_two_b_right_valid(service);

        assertTrue(service.do_biz());

    }

    //只写了正例

结果是:

1. 这样你直接针对最大方法的进行测试,没那么“单元”,但仍达到了你的测试目的,因为你只想验证y=f(a,b,c,d)在a,b,c,d 在各种取值时的正确性

2. Mock了四个方法(即判定树的叶结点),比原来的6个少了一些

3. 虽然只测了最大方法,但小方法的各种逻辑依然可以覆盖全,只要你穷举四个叶结点的真值表组合即可

Service层的单元测试:与其Mock别人,不如Mock自己

此为抛砖贴,希望能引出更好的见解。

你会怎么单元测试这样一个service层的类?

public class Service {

    
    private BizOne bizOne;
    
    private BizTwo bizTwo;

    public boolean doBiz(Object param) {

        Object resultOne = bizOne.getIt(param);
        if (resultOne == null) {
            return false;
        }
        return bizTwo.getIt(param) == null;

    }
    ...
}

不管怎么测,对bizOne和bizTwo的调用都得mock掉,否则你的测试类可能会直接访问数据库或者依赖容器。所以你可能会这样写测试代码:


        // do mocking
        BizOne bizOne = expectBizOneNotReturnNull(param);
        BizTwo bizTwo = expectBizTwoReturnNull(param);

        // inject mocked objects
        service.setBizOne(bizOne);
        service.setBizTwo(bizTwo);

        assertTrue(service.doBiz(param));

看起来不错,但有两个小问题:

  1. Mock的bizOne, bizTwo对象需要再通过setter方法注入到service对象中。一种测试场景就得写一次setter注入,多种测试场景就得写多次了,这会比较繁琐,而且可能会让测试代码失去重点。

  2. expectBizOneNotReturnNull, expectBizTwoReturnNull 跟业务逻辑无法直接对应。被测类背后的真实契约是“符合bizOne逻辑”且“符合bizTwo逻辑 => 返回true”,而不是“A为空,B不为空之类的”

何不改成这样:


        //do mocking
        expectBizOneValid(service, param);
        expectBizTwoValid(service, param);

        assertTrue(service.doBiz(param));

这里就不用再手工用setter拼装了,而且,契约感也很强:expectBizOneValid + expectBizTwoValid => assertTrue

你还顺便获得了一个好处:你会意识到,被测代码本身也应该增强契约感,应该这样改写:

public class Service {

    private BizOne bizOne;
    private BizTwo bizTwo;

    public boolean doBiz(Object param) {
        //用isBizOneValid()替代bizOne.getIt(param)!=null, 代码能更直接地反应业务逻辑,这也是《重构》这本书里极力推荐的作法
        if (!isBizOneValid(param)) {
            return false;
        }
        return isBizTwoValid(param);

    }

    //这里要用包可见性方法,因为测试类里要mock它
    boolean isBizTwoValid(Object param) {
        return bizTwo.getIt(param) == null;
    }

    boolean isBizOneValid(Object param) {
        Object resultOne = bizOne.getIt(param);
        return resultOne != null;
    }
 ...

现在Service本身的实现也很像契约了。当然,你也可以看出副作用: 只mock Service类的部分方法,这需要mock框架支持; 另外,isBizTwoValid()这种私有契约要设成包可见性以方便被同一个包下的测试类mock(否则就得javaagent + 反射,很麻烦)

好处和坏处都有,看你的口味了。最后总结一下 --关于“Mock Service这个类自己的方法,而不是Mock BizOne, BizTwo”这种做法

1.好处:

  a.Mock别人就意味着要做一次组装,有点烦

  b.Mock可以强迫被测类的内部接口更具契约感,测试类更具契约感

2.坏处

  a.Mock框架需支持Partial Mock (推荐jmockit)

  b.被Mock的内部方法要么设成包可见性(损失封装性),要么设成private并通过方式名反射来Mock(损失方法名的重构安全性)

ibatis: 能select出对象,但对象的每个属性都是null ?

这很有可能是因为:

  1. 你的select statement没用resultMap,但用了resultClass

  2. 而且你的select语句里没有使用 select
coloumn_name as
propertName 这种格式。 你可能省略了 as
propertName

另外,使用 "select
coloumn_name as
propertName"属于取巧的做法;官方文档建议用resultMap:

Using SQL aliases to map columns to properties saves defining a <resultMap> element, but there are limitations. There is no way to specify the types of the output columns (if needed), there is no way to automatically load related data such as complex properties, and there is a slight performance consequence from accessing the result metadata. Architecturally, using aliases this way mixes database logic with reporting logic, making the query harder to read and maintain. You can overcome these limitations with an explicit Result Map (Section 3.5).