iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
0
Software Development

如何一步步實踐TDD (測試驅動開發)系列 第 4

TDD 範例二:物件 (PHP)

這個範例讓我們來看看怎麼用 TDD 來寫物件,不過我寫完程式之後才發現都沒看到什麼需要Refactor的地方,有點可惜沒有演示到這部分。

複習 TDD 步驟:

  1. 寫測試: 在寫任何產品程式之前,只先編寫最少量、剛好能運作的自動化測試
  2. 寫程式: 編寫最少量、剛好能通過的產品程式
  3. 重構程式碼,並循環以上步驟

來寫一個可以計算多人薪水總和的程式。

思考之後預計會有一個Salary類別,代表一個人的名字與薪水,以及PaymentList類別,可以加入Salary物件之後計算總和。

完整程式碼可以在Github取得,用$ git checkout切換查看。

步驟 1.

第一個要寫的是Salary類別的 set 函式,類似範例一的呼叫函式,從最初的呼叫類別產生物件開始。

<?php
// Salary.php


// ---tests---
run_tests();

function run_tests(){
    test_set_salary();
}

function test_set_salary(){
    $tmp_salary = new Salary();
}

這次預期會有好幾個測試,因此會將每一段獨立的測試用一個 function 包起來,所有要跑的測試都放在run_tests()中執行。

在 Terminal 執行我們的新測試,理所當然地印出了錯誤。

$ php Salary.php
PHP Fatal error:  Uncaught Error: Class 'Salary' not found in Salary.php:13

步驟 2.

定義Salary類別。

<?php
// Salary.php

class Salary{
    
}

( 若有下載範例程式碼,用$ git checkout 2a查看 )


循環 2 - 步驟 1.

test_set_salary()要測試利用set_data()是否的確有設定了Salary物件的名字與薪水數值。

這個測試對於目前的物件來說其實意義不大,只要對語法有基本瞭解,大概就會知道這樣的設定當然沒問題。

但是當物件的設定函式越來越龐大時,或是之後對資料庫的存取,利用自動化測試確保函式的確有更新資料,是相當重要的。

<?php
// Salary.php

class Salary{

}

// ---tests---
run_tests();

function run_tests(){
    test_set_salary();
}

function test_set_salary(){
    $tmp_salary = new Salary();
    $tmp_salary->set_data("Louis", 100);
    assert($tmp_salary->name == "Louis");
    assert($tmp_salary->value == 100);
}
PHP Fatal error:  Uncaught Error: Call to undefined method Salary::set_data() in Salary.php:17

2 - 步驟 2.

set_data()將參數的名字跟數值設定到Salary的變數中 [1]。

<?php
// Salary.php

class Salary{
    public $name;
    public $value;

    public function set_data($_name, $_value){
        $this->name = $_name;
        $this->value = $_value;
    }
}

( $ git checkout 2b )


3 - 步驟 1.

寫完了Salary類別,開始進到PaymentList類別,在函示中產生 instance。

<?php
// PaymentList.php

require 'Salary.php';

...

// ---tests---
run_payment_list_tests();

function run_payment_list_tests(){
    test_insert_a_salary();
}

function test_insert_a_salary(){
    $tmp_salary = new Salary();
    $tmp_salary->set_data("Louis", 100);

    $tmp_payments = new PaymentList();
}
PHP Fatal error:  Uncaught Error: Class 'PaymentList' not found in PaymentList.php:18

3 - 步驟 2.

<?php
// PaymentList.php

require 'Salary.php';

class PaymentList{
    
}

熟練之後,對於語法夠熟悉、覺得這樣的循環太繁瑣的話,這樣只有函式、物件名稱的循環可以省略掉。


4 - 步驟 1.

test_insert_a_salary()測試Salary物件是否的確有被傳入PaymentList物件的$list變數中。

<?php
// PaymentList.php

...

// ---tests---
run_payment_list_tests();

function run_payment_list_tests(){
    test_insert_a_salary();
}

function test_insert_a_salary(){
    $tmp_salary = new Salary();
    $tmp_salary->set_data("Louis", 100);

    $tmp_payments = new PaymentList();
    assert(count($tmp_payments->list) == 0);

    $tmp_payments->insert_salary($tmp_salary);
    assert(count($tmp_payments->list) == 1);
    assert($tmp_payments->list[0]->name == "Louis");
}
PHP Notice:  Undefined property: PaymentList::$list in PaymentList.php on line 22

PHP Warning:  count(): Parameter must be an array or an object that implements Countable in PaymentList.php on line 22

Warning: count(): Parameter must be an array or an object that implements Countable in PaymentList.php on line 22

PHP Fatal error:  Uncaught Error: Call to undefined method PaymentList::insert_salary() in PaymentList.php:24

可以發現這邊印出的錯誤訊息竟然不止一個,不過不用擔心。

4 - 步驟 2.

insert_salary()$list新增一個 element。

<?php
// PaymentList.php

require 'Salary.php';

class PaymentList{
    public $list = array();

    public function insert_salary($_salary){
        $this->list[] = $_salary;
    }
}

( $ git checkout 2c )

5 - 步驟 1.

最後的test_calculate_sum()測試加總的結果是否相符。

<?php
// PaymentList.php

...

// ---tests---
run_payment_list_tests();

function run_payment_list_tests(){
    test_calculate_sum();
    test_insert_a_salary();
}

function test_calculate_sum(){
    $tmp_salary_L = new Salary();
    $tmp_salary_L->set_data("Louis", 100);

    $tmp_salary_B = new Salary();
    $tmp_salary_B->set_data("Bear", 150);

    $tmp_payments = new PaymentList();
    $tmp_payments->insert_salary($tmp_salary_L);
    $tmp_payments->insert_salary($tmp_salary_B);

    assert($tmp_payments->calculate_sum() == 250);
}

...

PHP Fatal error:  Uncaught Error: Call to undefined method PaymentList::calculate_sum() in PaymentList.php:33

5 - 步驟 2.

增加calculate_sum()函式。

<?php
// PaymentList.php

require 'Salary.php';

class PaymentList{
    
    ...
    
    public function calculate_sum(){
        $result = 0;
        for ($i = 0; $i<count($this->list); $i++){
            $result += $this->list[$i]->value;
        }
        return $result;
    }
}

( $ git checkout 2d )

完成!

最初開始使用 TDD 時,同樣的程式一定會寫比平常久,因為跟自己原本寫程式的習慣跟思考方式很不一樣,同時也許讀者還沒幫自己的程式寫過自動化測試,TDD 與自動化測試,都需要花時間去練習。

題外話,因為 TDD 重複進行著寫測試->不通過->寫程式->通過,也有人稱呼 TDD 這樣的步驟為「紅綠燈」。

讀者可能會觀察到,這次範例中的測試,其實有不少重複的部分,

因此我們可以透過 測試框架 來增進測試的能力、以及編寫的速度,下一篇就來介紹測試框架的使用吧。


附註

  1. 將類別的變數定義為public並不是個好作法,只是為了方便演示。

上一篇
TDD 的理由
下一篇
測試框架 (PHPUnit)
系列文
如何一步步實踐TDD (測試驅動開發)30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ytyubox
iT邦新手 5 級 ‧ 2019-09-19 12:36:30

這些小標題看不是很懂?

步驟 1.
步驟 2.
循環 2 - 步驟 1.
2 - 步驟 2.
3 - 步驟 1.
3 - 步驟 2.
4 - 步驟 1.
4 - 步驟 2.
5 - 步驟 1.
5 - 步驟 2.
完成!
Louis iT邦新手 5 級 ‧ 2019-09-20 08:59:37 檢舉

每次的 [步驟 1.] 是指寫測試,[步驟 2.],是寫程式,
[步驟 1.] 跟 [步驟 2.] 進行了一次 TDD,[循環 2 - 步驟 1.]開始第二次 TDD,[3-步驟 1.] 則是第三次,
可能省略了一些字,看起來語意不統一,之後的文章試著換個比較清楚的方式說明。

我要留言

立即登入留言