細心一點的朋友們,或許會發現昨天有個細節沒討論到:「依賴的元件如果因為某些原因而無法初始化的話,該怎麼辦?」,這其實是一個常發生的問題,只是常常用不同的形式呈現。比方說:
上述問題都是因為依賴的元件還沒好,所以無法確認自己的程式是否有問題。
問題遇到了總是要解決,一種最直接的解決方法,同時也是大家最不樂見卻又很常使用的:「等待」。等依賴的元件完成再來整合自己的程式。這個方法有幾個缺點:
為了不要浪費在等待、為了能提前做驗證,又出現了另一種常見的做法:「規格書」等,通常會使用傳統檔案格式,如 Office 文件或 Wiki 等,來規定兩個元件間的互動方式。文件裡預定義了互動方式,讓雙方都能對互動行為做適當的測試。比方說文件有一個可以接受 POST 與 FormData 並回傳 JSON 資料的 API。對依賴方而言,可以假設 API 回傳的資料為正確的 JSON 資料並實作在程式裡,就能執行後續的測試了;對被依賴方而言,只要實作出符合規格書的程式即可。
這方法看似合理,不過也有潛在的問題:
寫文件有維護問題的話,那不如把假資料寫到測試程式裡好了。
比方說以昨天的例子, Number 物件預定要實作了 mux()
的方法,但還沒實作出來,但另一個要實作平方的物件 Square 要開始開發測試了。那測試可以這樣寫:
<?php
class SquareTest extends \Codeception\Test\Unit
{
public function testShouldGet100WhenParamsIs10()
{
// Arrange
$number = new NumberFake(10);
$target = new \HelloWorld\Square();
$excepted = 100;
// Act
$actual = $target->square($number);
// Assert
$this->assertEquals($excepted, $actual);
}
}
class NumberFake extends \HelloWorld\Number
{
public function mux()
{
return 100;
}
}
Square 程式:
<?php
namespace HelloWorld;
class Square
{
public function square(Number $number)
{
return $number->mux($number->get());
}
}
上面可以看得出,程式會丟 10
進 mux() 方法,而 NumberFake
會實作預期的假設值,參考乘法的規格書, 10 * 10 要回傳 100
,這樣就讓後續的測試可以繼續執行。
其他如 API 串接的狀況大同小異,只要還沒實作,但使用類似的方法回傳符合規格書上的值,都算相同的情況
可是這樣也有問題,實作這些東西可讀性不好,又會有命名衝突問題。不知道有沒有工具能解決這個問題?
有的,只要是要實作這些測試用的假程式假資料等,都通稱叫 Test Double!
看完上述問題與解決過程,相信大家了解 Test Double 的目的是要解決測試遇到的依賴問題。 Test Double 的中文叫「測試替身」,正如其名,它是測試才會用的替身。如果把測試案例當成是在演戲的話,正牌的物件就是「演員」,假的物件則是「替身」。它們跟演員和替身的關係一樣,替身有某些部分跟演員很像,但看戲的人或是使用物件的 Client 應該是無感的。
Test Double 有五種類型,這兩天會說明這幾種類型的實作與應用範例。
有時,測試案例執行測試( Act )的時候,並不會執行到依賴。但初始化目標物件會需要依賴物件,這時可以使用 Dummy Object 。
舉個例子: Number 實作了 save()
與 load()
方法可以把數值存入資料庫或讀取,而資料庫連線是建構的時候傳入。程式實作如下:
<?php
namespace HelloWorld;
use PDO;
class Number
{
private $number;
private $pdo;
public function __construct($number, PDO $pdo)
{
$this->number = $number;
$this->pdo = $pdo;
}
public function add($addend)
{
return $this->number + $addend;
}
public function sub($subtrahend)
{
return $this->number - $subtrahend;
}
public function get()
{
return $this->number;
}
public function save()
{
// Use PDO
}
public function load()
{
// Use PDO
}
}
從程式可以看出,當我在測 add()
sub()
get()
時,跟 PDO 完全無關。這時可以用 Dummy Object ,測試程式範例如下:
<?php
class NumberTest extends \Codeception\Test\Unit
{
public function testShouldGet1WhenConstructArgIs1()
{
// Arrange
$pdoMock = \Codeception\Util\Stub::make('PDO');
$target = new \HelloWorld\Number(1, $pdoMock);
$excepted = 1;
// Act
$actual = $target->get();
// Assert
$this->assertEquals($excepted, $actual);
}
}
因為原程式的建構子修改,所以其他測試也需要修改,這邊就不佔篇幅了。
這裡可以發現,雖然建構子被限制要 PDO 物件才能傳入,但程式還是能執行,因為它實際的運作原理正是使用繼承。
有時會需要假物件在被執行某個方法時,固定回傳某個值,有點像 HardCode 的概念。 Stub 正是可以簡單實作出這樣的功能。
回頭看最上面的 Square 的 Example ,它正是需要回傳固定值的案例,我們來把它改成 Stub 。首先,要被替身的程式要先定義 mux() 方法:
<?php
namespace HelloWorld;
class Number
{
// ...
public function mux()
{
// Not implement;
}
// ...
}
再來就是我們 Stub 上場了
<?php
class SquareTest extends \Codeception\Test\Unit
{
public function testShouldGet100WhenParamsIs10()
{
// Arrange
$numberMock = \Codeception\Util\Stub::make(\HelloWorld\Number::class, ['mux' => 100]);
$target = new \HelloWorld\Square();
$excepted = 100;
// Act
$actual = $target->square($numberMock);
// Assert
$this->assertEquals($excepted, $actual);
}
}
我們也可以很無聊試看看在 Assert 加入
$this->assertEquals(100, $numberMock->mux();
測看看是不是真的拿到 100 。
今天講了五種類型的兩種,範例程式可以在 GitHub 找到,明天會再講其他三種 Test Double 。