建立存根

2003 年 6 月 10 日

測試增強設計中常見的問題是,如何在測試模式下建立服務存根,同時讓實際服務在生產環境(以及某些測試)中運作。我的幾位同事分享了他們的點子。

Jeremy Stell-Smith 向我展示了一種基於抽象工廠的方法。所有可存根的服務都從單一工廠提取。此範例顯示了這樣的 Persistance 類別。

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 中。

這種做法類似於模擬物件專家在模擬物件上設定預期的想法。您可以將這視為執行模擬物件的一種輕量級方式。