接下來要 inject dependency object 啦~
Constructor & Setter Injection Constructor Injection 在被測試 class 加新的 constructor 或在原本的 constructor 加新參數,傳進剛抽出來的 interface 的 object,將它存在被測試 class 的 member,被測試 class 裡的程式邏輯使用這個 member 做事。
IFoo.h 1 2 3 4 5 6 7 8 #pragma once class IFoo {public : virtual ~IFoo () = 0 ; virtual int bar () = 0 ; }; inline IFoo::~IFoo () { }
Foo.h 1 2 3 4 5 6 7 8 9 #pragma once #include "IFoo.h" class Foo : public IFoo { public : int bar () { }; };
MyClass.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "IFoo.h" class MyClass {public : MyClass (IFoo *pFoo) : m_pFoo (pFoo) { }; ~MyClass () { delete m_pFoo; } void DoSomething () { m_pFoo->bar (); }; private : IFoo *m_pFoo; };
FakeFoo.h 1 2 3 4 5 6 7 8 9 #pragma once #include "IFoo.h" class FakeFoo : public IFoo { public : int bar () { return 1 ; }; };
main.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include "MyClass.h" #include "Foo.h" #include "FakeFoo.h" int main () { MyClass obj (new Foo()) ; obj.DoSomething (); MyClass testObj (new FakeFoo()) ; testObj.DoSomething (); return 0 ; }
不同語言對 function 參數以及 constructor 的支援不一,例如 function 參數能不能有預設值、能不能不傳部份參數、constructor 可以有幾個等等,會影響 constructor injection 的實作方式。
如果 function 參數可以有預設值,以 PHP 為例,實作可以變形成在 constructor 加一個預設值為 null
的參數,在 constructor 中判斷該參數是否為 null
,是則產生 depedency object,否則使用傳進來的 object。如此在 production 程式中產生 object 可以不傳參數(假設沒有其他非傳的參數),只在測試中才傳 stub 進被測試 class。這個作法可以不動到 production code 已經使用的被測試 class,在已經寫了 production code 後才加測試很好用。當然這讓 constructor 的參數有 optional 的意思,而非產生 object 的必須參數。
MyClass.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php class Foo { public function bar ( ) { } } class MyClass { private $foo ; public function __construct ($foo = null ) { if ($foo ) { $this ->foo = $foo ; } else { $this ->foo = new Foo ; } } public function DoSomething ( ) { $this ->foo->bar (); } } $myObj = new MyClass ; $myObj ->DoSomething ();class FakeFoo { public function bar ( ) { } } $testObj = new MyClass (new FakeFoo ); $testObj ->DoSomething ();
C++ 雖然也支援 function 參數預設值,但我不太喜歡用(它在繼承上沒寫好會有些問題),多個 constructor 的例子:
MyClass.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "IFoo.h" #include "Foo.h" class MyClass {public : MyClass () : m_pFoo (new Foo ()) { }; MyClass (IFoo *pFoo) : m_pFoo (pFoo) { }; ~MyClass () { delete m_pFoo; } void DoSomething () { m_pFoo->bar (); }; private : IFoo *m_pFoo; };
main.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include "MyClass.h" #include "FakeFoo.h" int main () { MyClass obj; obj.DoSomething (); MyClass testObj (new FakeFoo()) ; testObj.DoSomething (); return 0 ; }
這可以看到 constructor injection 的一個問題:當有愈來愈多 dependency,在 constructor 加入參數或加新的 constructor 會變得愈來愈困難,過多的參數或 constructor 也會降低可讀性。constructor 有愈多參數,參數間如果也有 dependency 關係,constructor 本身的邏輯可能會變得比較複雜。解決這個問題有幾種方式:用 parameter object refactoring 或 Inversion of Control (IoC) container。不過我比較喜歡從設計上就讓 class 間的相依關係不那麼複雜。
Setter Injection 使用被測試 class 的 setter 來 inject 假物件。
就……就是用 setter,有點懶得寫例子了。(欸
可測試性與 class 語意 加 constructor、constructor 加參數、加 setter 都會稍微改變 class 的語意,目前我傾向以改變最小語意的方式增加可測試性,讓測試描述的被測試 class 使用情境跟 production code 一樣(因為先寫了 production code 才寫測試,會比較以 production code 為主)。如果 production code 不需要 setter 會優先使用 constructor injection,如果本來有 setter 則依照 class 使用方式用 setter injection。
以 factory 取得 dependency object 的 class 如果被測試程式以 factory 的 static function 產生 dependency object,難以使用 constructor 跟 setter injection。
怎麼辦咧?在 factory 增加 setter 設定 factory 回傳的物件好在測試中塞假物件到 factory。不過在 factory 加 setter 有些破壞封裝性,可以用些語言特性限制 setter 的 access 範圍(如 C# 的 internal),或者用 #define
跟 #ifdef
讓測試相關的 code 只存於 debug 模式(這寫法的 code 挺難看的),又或者把 setter 取名為 SetXXXForTest()
。
由於 factory 用了 static member(讓 setter 存 inject 的 object),test case 的前或後(setup 或 teardown)要還原 factory 的狀態,不然測試間可能會互相干擾。
繼續用例子來看,一般使用 factory 取得 dependency object 的情況:
FooFactory.h 1 2 3 4 5 6 7 8 9 #pragma once #include "Foo.h" class FooFactory {public : static IFoo* createFoo () { return new Foo (); } };
MyClass.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "IFoo.h" #include "FooFactory.h" class MyClass {public : MyClass () { m_pFoo = FooFactory::createFoo (); }; ~MyClass () { delete m_pFoo; } void DoSomething () { m_pFoo->bar (); }; private : IFoo *m_pFoo; };
加 setter 的 factory:
FooFactory.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #pragma once #include "Foo.h" class FooFactory {public : static IFoo* createFoo () { if (m_pFoo) { return m_pFoo; } return new Foo (); } static void SetFoo (IFoo *foo) { m_pFoo = foo; } private : static IFoo *m_pFoo; }; IFoo* FooFactory::m_pFoo = NULL ;
main.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include "MyClass.h" #include "FakeFoo.h" int main () { FooFactory::SetFoo (new FakeFoo ()); MyClass obj; obj.DoSomething (); FooFactory::SetFoo (NULL ); MyClass obj2; obj2. DoSomething (); return 0 ; }
系列文章