建立存根
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 中。
這種做法類似於模擬物件專家在模擬物件上設定預期的想法。您可以將這視為執行模擬物件的一種輕量級方式。