Upgrade to Pro — share decks privately, control downloads, hide ads and more …

單元測試經驗談

johnroyer
September 21, 2020

 單元測試經驗談

從開始撰寫安源測試以後,發現並不是所有的程式都有辦法寫測試。
先撰寫優良的程式碼,才能建立完整的測試。

tag: software testing, unit test, OOP principle, design pattern

johnroyer

September 21, 2020
Tweet

More Decks by johnroyer

Other Decks in Programming

Transcript

  1. 軟體測試  Garbage In Garbage Out (GIGO)  透過測試能及早發現程式、環境異動或錯誤 

    交接速度快,不怕同事請假 ( 程式寫錯就會發出警告 )  好的測試可以當作程式說明文件使用 (test case as document)
  2. 軟體測試  黑箱測試  整合測試 (integration test)  系統測試 (system

    test)  效能測試 (performance test)  猴子測試 (monkey test) .... etc.
  3. 軟體測試  白箱測試  單元測試 (Unit test)  功能測試 (functional

    test)  整合測試 (integration test) 本次介紹的內容 以 PHP 作範例
  4. 單元測試 (Unit Test) class Counter { function getBmi() { }

    function isTooFat() {} } not only one unit
  5. 單元測試 (Unit Test)  程式碼越單純,測試越好寫 function getString($input) { return "$input";

    } function testStringGetter() { $result = getString(123); $this- >assertTrue(is_string($result)); }
  6. 單元測試 (Unit Test) function checkInputScope($x) { return (2 <= $x)

    && ($x =< 5); } function testBiggerThen5() { } function testSmallerThen2() { } ..... 一個條件判斷式 就需要二個或是以上的測試
  7. 單元測試 (Unit Test)  還需要測試:  try ... catch &

    exceptions  in & out data structure  不同的 data type (weak data typing language)
  8. 單元測試 (Unit Test) function checkInputScope($x) { if (($x <= x)

    && ($x =< 5)) { throws new \Exception(); } } /** @expectedException \Exception */ function testExceptionThrows() { }
  9. 單元測試 (Unit Test) function range($min, $max, $step) { ...... return

    $numberList; } function testOutputDataType() { $this->assertTrue( is_array(range(1, 10, 1)) ); }
  10. 單元測試 (Unit Test)  Type Hint, with PHP >= 7.1

    declare(strict_types=1); function range( int $min, int $max, int $step, ):array { }
  11. 撰寫單元測試  常用的 assertions  assertTrue / False  assertEquals

     assertGreaterThen  assertLessThen  assertEmpty  assertInstanceOf  assertIsArray  assertContains  assertNotContains
  12. 撰寫單元測試  測試案例 (test case) 的寫法 public function testGetter() {

    $o = new MyLib(); $result = $o->get(); $this->assertEquals(null, $result); }
  13. 撰寫單元測試  測試案例 (test case) 的寫法 public function testGetter() {

    $o = new MyLib(); $result = $o->get(); $this->assertEquals(null, $result); } 執行一次 測試對象
  14. 撰寫單元測試  測試案例 (test case) 的寫法 public function testGetter() {

    $o = new MyLib(); $result = $o->get(); $this->assertEquals(null, $result); } 驗證預期結果與直營結果是否相符合
  15. 撰寫單元測試 /** @depends testPush */ public function testPop($stack) { $out

    = $stack->pop(); // assertion return $out; } 前一個測試後的執行結果
  16. 撰寫單元測試 /** @depends testPush */ public function testPop($stack) { $out

    = $stack->pop(); // assertion return $out; } 繼續往下接 ....
  17. 撰寫單元測試  為何不在 testPoP() 中 將 $stack 與 $out 一次測試完成?

     可以,做得到!  一個測試案例應該只負責一件工作 ( 好維護 )  若有特殊狀況 也盡可能在測試案例中維持個位數的 assertion ( 測試結果比較容易看懂 )
  18. 撰寫單元測試  Fixtures  建立待測對象 (data structure / object) 並初始化

     測試對象需要在每個 test case 執行前 reset  測試開始前給定測試用資料  降低不同 test case 互相影響 ( 依賴 ) 產生的問題
  19. 撰寫單元測試 public function setUp() { $this->stack = new SplStack(); $this->stack->push(1);

    $this->stack->push(2); } public function tearDown() { $this->stack = null; }
  20. 撰寫單元測試 public function setUp() { $this->stack = new SplStack(); $this->stack->push(1);

    $this->stack->push(2); } public function testStack() { $this->stack->count(); // int(2) }
  21. 撰寫單元測試 public function test1() { $this->stack->push('str'); $this->stack->count(); // int(3) }

    public fcuntion test2() { $this->stack->count(); // int(2) } ---- $this->stack 會在 test case 執行完成以後被 reset -----
  22. 撰寫單元測試  會用到 fixture 的情形  受測對象中不同的狀態 (state) 例如: NEW,

    DRAFT, PUBLISHED  Test case 執行後會導致受測對象狀態異動  受測對象需要初始化 或是要事先給定資料
  23. Private Method / Variable class User { private height =

    160; } $c = new ReflectionClass($user); $p = $c->getProperty('height'); $p->setAccessible(true); $this->assertEquals(160, $p->getValue());
  24. Private Method / Property  Private method / property 不需另外測試

     另一派的解釋  一般情況下會被其他 public method 呼叫  public method 所有 condition 走完一次 也表示 private method 也被執行過一遍 ( 高內聚 )
  25. Dummy Object  應付特定需求 (data type ... etc.)  對測試沒什麼影響

    class DummyUser extends User {} $user = new DummyUser(); checkUserInfo(User $user);
  26. Test Doubles  功能越簡單越好  不要為了 test doubles 再寫 unit

    test class FakeMemcacahe { function get($key) {}; function set($key, $val) {}; function keys() {}; ..... }
  27. 單元測試 (Unit Test)  並非所有的 code 都有辦法建立 test case 

    Unit test 通過只能表示各項小功能正確 無法代表整個專案可以正確執行  靠整合測試來檢查整體行為是否如預期
  28. 過於複雜的 constructor  Constructor 指負責資料初始化  不要與其他 class 、資料掛勾 function

    __construct() { try { $this->user = Order::last()->UID(); } catch (Exception $e) { throw $e; } }
  29. Static  Static class / function 無法被 mock function __construct()

    { try { $this->user = Order::last()->UID(); } catch (Exception $e) { throw $e; } }
  30. Conditions / Loops  巢狀 conditions / loops if ($id

    = 'xxx') { if ($password = 'xxx') { return true; } else { return false; } } else { return false; } 4 個測 試
  31. Conditions / Loops  使用 short-circuit 改寫 if ($id !=

    'xxx') { return false; } if ($password != 'xxx') { return false; } // login successed 提早跳脫
  32. Globals  可以使用全域變數,但有 side effect 較難測試 function isUserValid() { global

    $uid; $list = getAllUser(); return in_array($uid, $list); } 會改到 $uid ?
  33. SOLID Principles in OOP  Single responsibility  Open-closed 

    Liskov Substitution  Interface Segregation  Dependency Inversion
  34. Single responsibility Class User { function getUserById(); function getUserByName(); function

    createUser(); function getUserByOrderId(); } 歪了 可能要建立 stub
  35. Open-closed class User { protected $users = []; } class

    UserList extends User implements Traversable { protected $currentIndex = 0; function current(); function next(); ....
  36. Open-closed class TaskQueue { public $tasks; function pop(); function push();

    } $queue->$task[] = new Task(); 違反封閉原則
  37. Open-closed class TaskQueue { private $tasks; function pop(); function push();

    } $queue->push(new Task()); 透過 interface 操作
  38. Liskov Substitution class Animal {} class Food {} class DogFood

    extends Food {} class CatFood extends Food {}
  39. Liskov Substitution class Dog extends Animal { public function eat($food)

    { if (!$food instanceof DogFood) { throw new DogFoodException(); } class Cat extends Animal { public function eat(Food $food) { ... throw new CatFoodException();
  40. Liskov Substitution $dog = new Dog(); $cat = new Cat();

    try { $dog->eat($food); $cat->eat($food); } catch (DogFoodException $de) { ... } } catch (CatFoodException $ce) { ... }
  41. Liskov Substitution class Animal { public function eat (Food $food)

    {} } class Dog extends Animal { public function eat($food) { if (!$food instanceof DogFood) { parent::eat(); } }
  42. Interface Segregation class Dog implements DogInterface { public function walk()

    { return 'walking ...'; } public function sleep() { return '.. zz ZZ'; } }
  43. Interface Segregation class RobotDog implements DogInterface { public function walk()

    { return 'walking ...'; } public function sleep() { throw new Exception(); } }
  44. Dependency Inversion class User { function getByFile(File $reader) {} function

    getByAws(AwsS3 $reader) {} function getByDb(MySQL $reader) {} }
  45. Dependency Inversion class User { function getByFile(File $reader) {} function

    getByAws(AwsS3 $reader) {} function getByDb(MySQL $reader) {} } User 類別需要知道各種 reader 的實作方 式
  46. Dependency Inversion Interface ReaderInterface { function open(); function read(); function

    save(); } class File implements ReaderInterface; class AwsS3 implements ReaderInterface; class MySQL implements ReaderInterface;
  47. Dependency Inversion  僅相依在 interface ,容易 mock  上層類別 (User)

    不需考慮底層類別 (reader) 如何實作  底層類別改變實作方式不影響上層類別
  48. Dependency Injection class User { function get() { $dbConn =

    new PDO(); $users = $dbConn->exec('...'); } }
  49. Dependency Injection class User { function get() { $dbConn =

    new PDO(); $users = $dbConn->exec('...'); } } 不要自己 new 東西來用
  50. Dependency Injection class User { function get(UserReader $reader) { $users

    = $reader->read(); } } 請外部提供資料、方法