4

請考慮下面的代碼:如何模擬另一個類負責實例化的類?

type 
    TFoo1 = class 
    public 
    procedure DoSomething1; 
    end; 

    TFoo2 = class 
    private 
    oFoo1 : TFoo1; 
    public 
    procedure DoSomething2; 
    procedure DoSomething3; 
    constructor Create; 
    destructor Destroy; override; 
    end; 


procedure TFoo1.DoSomething1; 
begin 
    ShowMessage('TFoo1'); 
end; 

constructor TFoo2.Create; 
begin 
    oFoo1 := TFoo1.Create; 
end; 

destructor TFoo2.Destroy; 
begin 
    oFoo1.Free; 
    inherited; 
end; 

procedure TFoo2.DoSomething2; 
begin 
    oFoo1.DoSomething1; 
end; 

procedure TFoo2.DoSomething3; 
var 
    oFoo1 : TFoo1; 
begin 
    oFoo1 := TFoo1.Create; 
    try 
    oFoo1.DoSomething1; 
    finally 
    oFoo1.Free; 
    end; 
end; 

我創建單元測試類,我堅持就可以了。我的問題都是關於嘲笑對象的最佳方式以及我應該使用的設計模式。我是單元測試的課程不是由我創建的。

  1. 在下面的例子中,我需要模擬Foo1由於其發送到Web服務,我可以在我的單元測試期間不叫請求。但Foo1正在由TFoo2構造函數創建,我無法嘲笑它。在這種情況下我該怎麼辦?我是否應該修改TFoo2構造函數來接受這樣的對象Foo1

    constructor TFoo2.Create(aFoo1 : TFoo1) 
    begin 
        oFoo1 := aFoo1; 
    end; 
    

    是否存在一種設計模式,表示我們需要傳遞類所依賴的所有對象,如上例所示?

  2. 方法TFoo2.DoSomething3創建Foo1對象,然後釋放它。我是否也應修改該代碼以通過Foo1對象?

    procedure TFoo2.DoSomething3(aFoo1 : TFoo1); 
    begin 
        aFoo1 := aFoo1.DoSomething1; 
    end; 
    
  3. 有什麼設計模式支持我提出的建議嗎?如果是這樣,我可以告訴我工作的公司中的所有開發人員,我們需要遵循XXX模式,以便使單元測試更容易。

回答

8

如果你不能嘲笑的TFoo1創造,那麼你不能嘲笑TFoo1。目前,TFoo2負責創建TFoo1的所有實例,但如果這不是TFoo2的主要目的,那麼這確實會使單元測試變得困難。

一種解決方案是,如你所說,通過它需要TFoo2任何TFoo1實例。這可能使已經調用TFoo2方法的所有當前代碼複雜化。另一種方式是單元測試友好一點,就是爲TFoo1提供工廠。工廠可以像函數指針一樣簡單,也可以是整個類。在德爾福,metaclass也可以作爲工廠。在工廠建造時將工廠傳遞到TFoo2,並且每當TFoo2需要一個TFoo1實例時,它可以調用工廠。

爲了減少改變你的代碼的其餘部分,可以使出廠參數必須在TFoo2構造一個默認值。然後,您不必更改您的應用程序代碼。只需更改您的單元測試代碼即可提供非默認的工廠參數。

無論你做什麼,你都需要讓TFoo1.DoSomething1成爲虛擬的,否則嘲笑將是徒勞的。

使用元類,你的代碼可以是這樣的:

type 
    TFoo1 = class 
    procedure DoSomethign1; virtual; 
    end; 

    TFoo1Class = class of TFoo1; 

    TFoo2 = class 
    private 
    oFoo1 : TFoo1; 
    FFoo1Factory: TFoo1Class; 
    public 
    constructor Create(AFoo1Factory: TFoo1Class = nil); 
    end; 

constructor TFoo2.Create; 
begin 
    inherited Create; 
    FFoo1Factory := AFoo1Factory; 
    if not Assigned(FFoo1Factory) then 
    FFoo1Factory := TFoo1; 

    oFoo1 := FFoo1Factory.Create; 
end; 

現在,你的單元測試代碼,可以提供的TFoo1一個模擬版本,並通過它時,它會創建一個TFoo2

type 
    TMockFoo1 = class(TFoo1) 
    procedure DoSomething1; override; 
    end; 

procedure TMockFoo1.DoSomething1; 
begin 
    // TODO: Pretend to access Web service 
end; 

procedure TestFoo2; 
var 
    Foo2: TFoo2; 
begin 
    Foo2 := TFoo2.Create(TMockFoo1); 
end; 

元類的許多例子給出了基類虛擬構造函數,但這不是嚴格的埃森。如果構造函數需要虛擬調用 - 如果後代構造函數需要使用基類尚未完成的構造函數參數執行操作,則只需要有一個虛擬構造函數。如果後代(在本例中爲TMockFoo1)與其祖先完全相同,則構造函數不需要是虛擬的。 (還記得AfterConstruction已經是虛擬的,所以這是另一種讓後代進行額外操作而不需要虛擬構造函數的另一種方式。)

+0

非常好,謝謝。元類思想非常酷。 – 2011-05-02 13:48:25

+0

你知不知道是否有任何設計模式告訴我們,我們需要通過構造函數或其他方法傳遞對象依賴關係?它是裝飾者模式嗎? – 2011-05-02 14:46:09

+4

你在找什麼是依賴注入或控制反轉。您已經在TFoo2上創建了TFoo1的依賴關係。相反,您想通過接口或構造函數將TFoo2「注入」到TFoo1中。請記住,如果你在編寫單元測試時遇到困難,那麼你做得不對。 ;-) – 2011-05-02 15:08:42

相關問題