實作練習
當確認設計與規格後,我們就要按照規格來實作了。而 TDD 強調先寫出測試,然後針對測試寫出我們的程式碼,以求讓測試通過。當測試都通過了,我們的程式就完成了。
它的流程如下:
- 針對規格撰寫測試。
- 執行測試,先讓測試失敗。
- 撰寫能讓測試通過的程式碼。
- 通過測試後,繼續下一個規格。
程式架構
為了簡化說明,專注在重點上,我們只實作類別與它們的測試類別。
.
├── Cart.php
├── Product.php
└── tests
│ ├── CartTest.php
│ └── ProductTest.php
└── phpunit.xml
程式碼骨架
Cart.php
<?php
class Cart
{
public function addProduct(Product $product)
{
}
public function checkout()
{
}
}
class CartException extends Exception
{
}
Product.php
<?php
class Product
{
public function __construct($name, $price, $tag)
{
}
public function getName()
{
}
public function getPrice()
{
}
public function getTag()
{
}
}
tests/CartTest.php
<?php
require_once __DIR__ . '/../Cart.php';
require_once __DIR__ . '/../Product.php';
class CartTest extends PHPUnit_Framework_TestCase
{
}
tests/ProductTest.php
<?php
require_once __DIR__ . '/../Product.php';
class ProductTest extends PHPUnit_Framework_TestCase
{
}
第一個 TDD 循環
接下來挑選以下使用案例來寫測試:
購物車加入一個紅標商品 1 後結帳,出現「無法結帳」的錯誤訊息。
首先我們要在 CartTest
中加入 fixtures :
class CartTest extends PHPUnit_Framework_TestCase
{
private $cart = null;
private $products = [];
public function setUp()
{
$this->cart = new Cart();
$this->products = [
new Product('紅標商品 1', 200, 'R'),
new Product('紅標商品 2', 160, 'R'),
new Product('綠標商品 1', 80, 'G'),
new Product('綠標商品 2', 100, 'G'),
];
}
public function tearDown()
{
$this->cart = null;
$this->products = [];
}
}
接著我們來加入第一個測試:
public function testAddOneRedProudct()
{
$this->setExpectedException('CartException', '商品配對錯誤');
$this->cart->addProduct($this->products[0]); // 紅標商品 1
$this->cart->checkout();
}
寫好後執行它:
C:\project> phpunit
PHPUnit 4.2.5 by Sebastian Bergmann.
Configuration read from C:\project\phpunit.xml
FF
Time: 33 ms, Memory: 3.00Mb
There were 2 failures:
1) Warning
No tests found in class "ProductTest".
2) CartTest::testAddOneRedProudct
Failed asserting that exception of type "CartException" is thrown.
FAILURES!
Tests: 2, Assertions: 1, Failures: 2.
可以發現測試是不通過的。別擔心,這是 TDD 的起手式。接著我們要想想看如何讓測試通過,先試試作弊法:
class Cart
{
// ...
public function checkout()
{
throw new CartException('商品配對錯誤');
}
// ...
}
我們在 Cart::checkout
方法中直接丟出了 CartException
,然後執行測試:
phpunit
PHPUnit 4.2.5 by Sebastian Bergmann.
Configuration read from C:\project\phpunit.xml
.
Time: 30 ms, Memory: 2.75Mb
OK (1 test, 2 assertions)
測試通過了!雖然看起來很蠢,但至少我們確認了測試有偵測到 CartException
的發生。
第二個 TDD 循環
在進行下一個使用案例前,我們先來完成 Product
類別。
在 tests/ProductTest.php
加入以下測試:
public function testProductConstructorAndGetter()
{
$product = new Product('商品1', 245, 'R');
$this->assertEquals('商品1', $product->getName());
$this->assertEquals(245, $product->getPrice());
$this->assertEquals('R', $product->getTag());
}
測試後出現紅燈,所以我們把 Product
類別的 constructor 和 getter 補完:
<?php
class Product
{
private $name = '', $price = 0, $tag = '';
public function __construct($name, $price, $tag)
{
$this->name = $name;
$this->price = $price;
$this->tag = $tag;
}
public function getName()
{
return $this->name;
}
public function getPrice()
{
return $this->price;
}
public function getTag()
{
return $this->tag;
}
}
測試通過!現在我們有個最簡單的 Product
類別了。
第三個 TDD 循環
接著看第二個使用案例:
購物車加入一個紅標商品 1 及綠標商品 1 ,總金額應為 ($200 + $80) * 0.75 = $210 。
寫出它的測試:
public function testAddOneRedProudctAndOneGreenProduct()
{
$this->cart->addProduct($this->products[0]); // 紅標商品 1
$this->cart->addProduct($this->products[2]); // 綠標商品 1
$this->cart->checkout();
$this->assertEquals(210, $this->cart->getTotal());
}
執行測試:
C:\project> phpunit
PHPUnit 4.2.5 by Sebastian Bergmann.
Configuration read from C:\project\phpunit.xml
.E
Time: 55 ms, Memory: 3.00Mb
There was 1 error:
1) CartTest::testAddOneRedProudctAndOneGreenProduct
CartException: 商品配對錯誤
C:\project\Cart.php:19
C:\project\tests\CartTest.php:47
FAILURES!
Tests: 2, Assertions: 2, Errors: 1.
沒意外地出現非預期的異常。
接著就是重點,我們的目標是要讓這兩個測試都能通過。
我們來思考一下怎麼寫出可以通過的程式內容:
addProduct
將紅標商品和綠標商品分組存放。checkout
方法比對紅標商品數是否等於綠標商品數,相等則是可以結帳,不相等則丟出「商品配對錯誤」的異常。當可以結帳時,
checkout
計算出所有商品的金額總和,並將其乘以 0.75 。
接下來我們依據這些規則來完成程式碼:
<?php
class Cart
{
// 商品分標籤存放
private $products = [
'R' => [],
'G' => [],
];
// 總計
private $total = 0;
public function addProduct(Product $product)
{
// 加入商品時先看標籤
$this->products[$product->getTag()][] = $product;
}
public function checkout()
{
// 找出商品數
$redCount = count($this->products['R']);
$greenCount = count($this->products['G']);
// 比對紅標商品數是否等於綠標商品數
if ($redCount !== $greenCount) {
throw new CartException('商品配對錯誤');
}
}
public function getTotal()
{
// 回傳總計
return $this->total;
}
}
// ...
我們先處理加入商品的部份,這時候再執行測試,結果會變成:
C:\project> phpunit tests/CartTest.php
PHPUnit 4.2.5 by Sebastian Bergmann.
Configuration read from C:\project\phpunit.xml
.F
Time: 45 ms, Memory: 3.00Mb
There was 1 failure:
1) CartTest::testAddOneRedProudctAndOneGreenProduct
Failed asserting that 0 matches expected 210.
C:\project\tests\CartTest.php:48
FAILURES!
Tests: 2, Assertions: 3, Failures: 1.
現在錯誤訊息已經是比對總計的預期值和實際值,這表示我們確實成功進入商品配對成功的路徑了。
註:如果是使用 Git 的話,可以先把修改放在 staging area 中,再繼續下一步的修改。
接著做計算金額的部份:
public function checkout()
{
// 比對紅標商品數是否等於綠標商品數
$redCount = count($this->products['R']);
$greenCount = count($this->products['G']);
if ($redCount !== $greenCount) {
throw new CartException('商品配對錯誤');
}
// 計算紅標商品金額小計
foreach ($this->products['R'] as $product) {
$this->total += $product->getPrice();
}
// 計算綠標商品金額小計
foreach ($this->products['G'] as $product) {
$this->total += $product->getPrice();
}
// 打七五折
$this->total *= 0.75;
}
執行測試:成功!
練習
思考看看,如果
Product
類別是由其他人開發,但尚未完成時,該怎麼做?試著用先前教的 PHPUnit 知識並搭配 TDD 完成剩下的使用案例。
Cart
類別有些重複的程式碼,試著重構它。