制作存根

2003年6月10日

测试增强型设计的一个常见问题是如何在测试模式下创建服务存根,同时让真实的服务在生产环境(以及某些测试中)可用。我的几位同事分享了他们的想法。

Jeremy Stell-Smith 向我展示了一种基于抽象工厂的方法。所有可存根的服务都从一个工厂中提取。此示例展示了这样的持久性类。

public abstract class Persistence...
  public static Persistence getInstance() {
    return (Persistence)Factories.get(Persistence.class);
  }

  public abstract void save(Object obj);
 

除了抽象工厂功能外,测试工厂还具有一个实现堆栈的不错功能——这使得工厂的设置更加容易。

public class FooTest...
  public void setUp() {
    TestFactories.pushSingleton(Persistence.class, 
                                new MockPersistence());
  }

  public void tearDown() {
    TestFactories.pop(Persistence.class);
  }

  public void testSave() {
    Foo foo = new Foo();
    foo.save();
    ...
  }

public class Foo ...
  public void save() {
    Persistence.getInstance().save(this);
  }

在另一个项目中,Kraig Parkinson 展示了一种略有不同的方法。与使用单个抽象工厂不同,那些需要存根的服务使用原型。

public class MyFacade {
  private static MyFacade prototype;

  /**
   * Sets the instance of the facade that will be returned by the getInstance method
   * used by all clients of the facade.
   */
  public static void setFacade(MyFacade newPrototype) {
    prototype = newPrototype;
  }

  /**
   * Returns an instance of the facade, using the prototype if set, 
   * otherwise an instance of the facade is used during normal operation.
   */
  public static MyFacade getInstance() {
    if (prototype != null)
      return prototype;
    else
      return new MyFacade();
  }

要在测试中使用它,您可以执行以下操作。

public class MyClientTest extends junit.framework.TestCase {
  private class Client {
    public String speak(String input) {
      return MyFacade.getInstance().echo(input);
    }
    public void dance() {
      return MyFacade.getInstance().move();
    } 
  }
  public void testSpeak() {
    final String expectedInput = "bar";
    final String expectedOutput = "foo";

    MyFacade.setPrototype(new MyFacade() {
      public String echo(String input) {
        assertEquals(expectedInput, input);
        return expectedOutput;
      }
    }

    //Invoke code that'd invoke the facade, but remember to remove 
    // the prototype reference once you're done with it....
    try {
      final String actualOutput = new Client.speak(expectedInput);
      assertEquals(expectedOutput, actualOutput);
    } finally {
      MyFacade.setPrototype(null);
    }
  }

  public void testDance() {
    final StringBuffer proof = new StringBuffer();

    MyFacade.setPrototype(new MyFacade() {
      public void move() {
        proof.append("g");
      }
    }

    //Invoke code that'd invoke the facade, but remember to remove 
    // the prototype reference once you're done with it....
    try {
      new Client().move();
      assertTrue("move was not invoked", proof.length > 0);
    } finally {
      MyFacade.setPrototype(null);
    }
  }

在这种情况下,Kraig 在测试方法的 finally 块中清理资源。另一种选择(我承认这是我通常的做法)是将清理代码放在 tearDown 中。

舞蹈案例类似于 Mock Objects 人员对模拟对象设置期望的想法。您可以将其视为一种轻量级的 Mock Objects 方法。