查尔斯推荐的单体测试的六条规则:

  ● 先写测试

  ● 从不写第一次成功的测试

  ● 从空的或不能工作的用例开始

  ● 不要为做些可以让测试运行的琐屑的事情而担心

  ● 低耦合和可测试性是密切联系的

  ● 使用mock对象

  先写测试

  这是极限编程的格言,我的经验也说明这样做有效。首先编写测试以及可以让测试编译通过的足够的应用程序代码(不要太多哦!)。然后你运行测试来证明它无法工作(参照如下的第二点)。然后你写可以让那个测试通过的足够的代码(参照如下的第四点)。然后你写另外一个测试。

  这种方法的好处来自你写代码的方法。你代码的每一部分都是目标驱动的。为什么我要写这行代码啊?我写是为了这样测试可以运行。为了让测试通过我应该做些什么啊?我必须写这行代码。你会一直写一些让你程序可以完全工作的东西。

  此外,先写测试意味着你在开始编码前必须决定如何让你的代码可测试。由于你在有一个覆盖代码的测试前你不会写任何代码,你不会写任何不可测试的代码。

  从不写第一次成功的测试

  写完测试后,马上运行它。它应该失败。科学的本质是证伪。写一个第一次通过的测试不能证明任何东西。证明你的测试不是测试成功的绿色横条可以,而是从红色横条变成绿色的过程。每次我写了一个第一次正确运行的测试,我会怀疑它。没有代码会第一次正确运行的。

  从空的或不能工作的用例开始

  从什么地方开始往往是一个障碍点。你如果想着针对一个方法运行的第一个测试,可以选择一些简单甚至琐碎的。有没有一个环境可以让该方法返回null,或者一个空的集合,或者一个空的数组?先测试这个用例。你的方法是不是会到数据库查询点东西?然后可以测试如果查询一些不存在的东西会发生什么。

  通常从简单的测试写起,它们可以给你一个编写更复杂交互的良好起点。它们可以让你起步。

  不要为做些可以让测试运行的琐屑的事情而担心

  这样你根据第三点写了如下测试:


public void testFindUsersByEmailNoMatch() {
             assertEquals(
                        "nothing returned",
                        0,
                        new UserRegistry().findUsersByEmail("not@in.database").length);
}


  很显然,可以让这个测试通过的小量的代码如下:


public User[] findUsersByEmail(String address) {
            return new User[0];
}


  写这样的为了让测试通过的代码的自然反应可能会是,“这简直是欺骗!”。这不是欺骗,因为编写查询一个用户以确认他不在那里的代码往往会是在你开始查找用户时一种自然扩展。

  你真正要做的事情是通过加上简单的代码并使得测试从失败到成功来证明测试可以工作。然后,如果你写了testFindUsersByEmailOneMatch和testFindUsersByEmailMultipleMatches,测试是保护你并保证你不会在一些小的用例中改变行为,保证你不会偶然抛出异常或返回null。

  第三点和第四点共同为你提供一个测试的基础,从而让你在开始处理重要用例时候不会忘记一些琐碎用例。

  低耦合和可测试性是密切联系的

  在你测试一个方法时候你应该让测试仅仅测试该方法。你不想事情让事情膨胀起来,或者陷入维护噩梦。例如,如果你有一个后台是数据库的应用,你需要有一个可以确保数据访问层可以正常工作的一套单体测试。所以你会上移一个层次并开始测试和访问层“说话”的代码。你想要控制数据库层生成的东西。你可能还想要模拟一个数据库失败。

  好可以通过独立、低耦合的组件编写应用程序并使得你的测试可以生成“傻瓜”组件(参见如下的mock对象)从而让每个组件都可以相互“交流”。这样可以使得你编写好应用的一部分的时候可以全面测试,即使你要编写的组件会依赖于一些不存在的东西。

  把你的应用划分成组件。将各个组件表示成相对于应用其他部分的接口,并尽量限制接口的范围。但一个组件需要给另外一个组件发消息的时候,考虑将其实施成和EventListener类似的注册/发布关系。你会发现这会使得测试更容易并很可能使得代码更容易维护。

  使用mock对象

  mock对象是一中可以模仿某个特定类型的对象,实际上是一个可以记录下调用它的方法的接收端。在这里可以找到一个我通过java.lang.reflect.Proxy类写的mock对象的实现,不过我还是相信其他地方还有一些更强大的实现。

  mock对象可以使得你能够测试隔离的组件,因为它给你一个在他们交互时一个组件对另一个组件的操作的清晰的视角。你可以在没有使用实际用户注册组件的情况下清楚地看到你所测试的组件调用了用户注册组件的“removeUser” 以及输入参数是“cmiller”。

  mock对象的一个有效应用是在不使用EJB容器的情况下测试会话EJB 。这里有一个测试类检查了一个会话EJB是否可以在发生错误的时候可以回滚所包含的交易。注意我是如何传入一个工厂到EJB——这是一个你可以实现在测试和部署时切换实现经常使用到的方法。


import org.pastiche.devilfish.test.*;
import junit.framework.*;
import java.lang.reflect.*;
import javax.ejb.SessionContext;
public class MonkeyPlaceTest extends TestCase {
            private static class FailingMonkeyFactoryHandler
                         implements InvocationHandler {
                 public Object invoke(Object proxy, Method method, Object[] args)
                         throws Exception {
                         if (method.getName().equals("createMonkey"))
                                    throw new MonkeyFailureException("Could not create");
                        
                         throw new UnsupportedOperationException("Didn't expect that");
                 }
            }
            public void testFailedCreate() throws Exception {
                 MockObjectBuilder factoryBuilder = MockObjectBuilder.getInstance(
                         MonkeyFactory.class,
                         new FailingMonkeyFactoryHandler());
                 MockObjectBuilder contextBuilder = MockObjectBuilder.getInstance(
                         SessionContext.class);
                 MonkeyPlaceBean bean = new MonkeyPlaceBean();
                 bean.setSessionContext(
                         (SessionContext)contextBuilder.getObject());
                 try {
                         bean.newMonkey(
                                (MonkeyFactory)factoryBuilder.getObject(),
                                "fred",
                                "bananas");
                         fail("Expected monkey failure exception");
                 } catch (MonkeyFailureException e) {
                         assertEquals("session was rolled back",
                                 new MethodCall("setRollbackOnly"),
                                 contextBuilder.getHandler().getMethodCall(0).getName();
                 }
            }
}