スタブの作り方

2003年6月10日

テストを強化した設計における一般的な問題は、テストモードでサービススタブを作成する方法であり、本番(および一部のテスト)で実際のサービスを使用できるようにすることです。私の同僚数名が自分のアイデアを共有してくれました。

Jeremy Stell-Smithはアブストラクトファクトリに基づくアプローチを教えてくれました。スタブ可能なすべてのサービスは、1つのファクトリからプルされます。この例では、そのような永続化クラスを示します。

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);
  }

別のプロジェクトでは、クラッグ・パーキンソンが少し異なる見解を示しています。単一のアブストラクトファクトリを使用するのではなく、スタブが必要なサービスはプロトタイプを使用します。

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);
    }
  }

この場合、クラッグはテストメソッドの一部としてfinallyブロックでリソースをクリーンアップします。もう1つの選択肢(これが自分のやり方であることは認めます)は、クリーンアップコードをtearDownに入力することです。

ダンスケースは、モックオブジェクトの人たちがモックオブジェクトの期待を設定するというアイデアと似ています。これは、モックオブジェクトを実行する軽量な方法と考えることができます。