Slide 1

Slide 1 text

軟體測試經驗談 日落 Zero 2020/09/27

Slide 2

Slide 2 text

Agenda  軟體測試  單元測試  撰寫可測試的程式

Slide 3

Slide 3 text

About me  日落 Zero  @zeroplex  PHP  Linux  Music

Slide 4

Slide 4 text

Working at ERP eCommerce CRM

Slide 5

Slide 5 text

軟體測試

Slide 6

Slide 6 text

軟體測試  Garbage In Garbage Out (GIGO)  透過測試能及早發現程式、環境異動或錯誤  交接速度快,不怕同事請假 ( 程式寫錯就會發出警告 )  好的測試可以當作程式說明文件使用 (test case as document)

Slide 7

Slide 7 text

軟體測試  黑箱測試:看不到程式碼  白箱測試:看得到程式碼並針對其做測試

Slide 8

Slide 8 text

軟體測試  黑箱測試  整合測試 (integration test)  系統測試 (system test)  效能測試 (performance test)  猴子測試 (monkey test) .... etc.

Slide 9

Slide 9 text

軟體測試  白箱測試  單元測試 (Unit test)  功能測試 (functional test)  整合測試 (integration test)

Slide 10

Slide 10 text

軟體測試  白箱測試  單元測試 (Unit test)  功能測試 (functional test)  整合測試 (integration test) 本次介紹的內容 以 PHP 作範例

Slide 11

Slide 11 text

單元測試

Slide 12

Slide 12 text

單元測試 (Unit Test)  "Unit" 代表最小的程式單位  一個簡單的 function / method  簡單的判斷或迴圈  小檔案

Slide 13

Slide 13 text

單元測試 (Unit Test) function getBmi(height, weight) { return weight / pow(height / 100, 2); } one unit

Slide 14

Slide 14 text

單元測試 (Unit Test) class Counter { function getBmi() { } function isTooFat() {} } not only one unit

Slide 15

Slide 15 text

單元測試 (Unit Test)  程式碼越單純,測試越好寫 function getString($input) { return "$input"; } function testStringGetter() { $result = getString(123); $this- >assertTrue(is_string($result)); }

Slide 16

Slide 16 text

單元測試 (Unit Test)  使用正確的資料進行測試 y = x + 1 , 2 <= x <= 5 y x 2 5

Slide 17

Slide 17 text

單元測試 (Unit Test)  找臨界值進行測試 y = x + 1 , 2 <= x <= 5 y x 2 5

Slide 18

Slide 18 text

單元測試 (Unit Test)  使用錯誤的資料進行測試 y = x + 1 , 2 <= x <= 5 y x 2 5

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

單元測試 (Unit Test)  撰寫測試 (test case) 通常會花掉比寫原始碼更多的時間  程式的設計、開發 會影響到撰寫測試的困難度

Slide 21

Slide 21 text

單元測試 (Unit Test)  還需要測試:  try ... catch & exceptions  in & out data structure  不同的 data type (weak data typing language)

Slide 22

Slide 22 text

單元測試 (Unit Test) function checkInputScope($x) { if (($x <= x) && ($x =< 5)) { throws new \Exception(); } } /** @expectedException \Exception */ function testExceptionThrows() { }

Slide 23

Slide 23 text

單元測試 (Unit Test) function range($min, $max, $step) { ...... return $numberList; } function testOutputDataType() { $this->assertTrue( is_array(range(1, 10, 1)) ); }

Slide 24

Slide 24 text

單元測試 (Unit Test)  Type Hint, with PHP >= 7.1 declare(strict_types=1); function range( int $min, int $max, int $step, ):array { }

Slide 25

Slide 25 text

單元測試 (Unit Test)  PHP unit test 工具  PHPUnit  Mockery  AspectMock

Slide 26

Slide 26 text

撰寫單元測試 class MyTest extends TestCase { public function testGetter() {} public function testSetter() {} }

Slide 27

Slide 27 text

撰寫單元測試 use PHPUnit\Framework\TestCase; class MyTest extends TestCase { public function testGetter() {} public function testSetter() {} }

Slide 28

Slide 28 text

撰寫單元測試  使用 assertion 判斷測試結果 public function testGetter() { $this->assertTrue($success); }

Slide 29

Slide 29 text

撰寫單元測試  常用的 assertions  assertTrue / False  assertEquals  assertGreaterThen  assertLessThen  assertEmpty  assertInstanceOf  assertIsArray  assertContains  assertNotContains

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

撰寫單元測試  測試案例 (test case) 的寫法 public function testGetter() { $o = new MyLib(); $result = $o->get(); $this->assertEquals(null, $result); } 驗證預期結果與直營結果是否相符合

Slide 33

Slide 33 text

撰寫單元測試  Test case 會出現的東西  Expected result: 預期結果  Actual result: 實際執行結果

Slide 34

Slide 34 text

撰寫單元測試 $stack = new SplStack(); $stack->push(); $stack->pop();

Slide 35

Slide 35 text

撰寫單元測試 $stack = new SplStack(); $stack->push(); $stack->pop(); 這二者會互相影響 ( 狀態變化 )

Slide 36

Slide 36 text

撰寫單元測試 public function testPush() { $stack->push('some thing'); $this->assertTrue( 1, $stack->count(), ); return $stack; }

Slide 37

Slide 37 text

撰寫單元測試 public function testPush() { $stack->push('some thing'); $this->assertTrue( 1, $stack->count(), ); return $stack; }

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

撰寫單元測試 /** @depends testPush */ public function testPop($stack) { $out = $stack->pop(); // assertion return $out; } 繼續往下接 ....

Slide 40

Slide 40 text

撰寫單元測試 /** @depends testPop */ public function testPopResult($out) { // assert: // $out should be int(1) }

Slide 41

Slide 41 text

撰寫單元測試  為何不在 testPoP() 中 將 $stack 與 $out 一次測試完成?  可以,做得到!  一個測試案例應該只負責一件工作 ( 好維護 )  若有特殊狀況 也盡可能在測試案例中維持個位數的 assertion ( 測試結果比較容易看懂 )

Slide 42

Slide 42 text

撰寫單元測試 其中一個 assertion 出現預期外的狀況 整個測試案例就會被中斷

Slide 43

Slide 43 text

撰寫單元測試  測試多組不同的資料  正確的資料  錯誤的資料  .....

Slide 44

Slide 44 text

撰寫單元測試  測試多組不同的資料 使用 data provider 來自動餵資料

Slide 45

Slide 45 text

撰寫單元測試 testIntegerChecker($expected, $input) { $this->assert( $expected, is_int($input), ); }

Slide 46

Slide 46 text

撰寫單元測試 testIntegerChecker($expected, $input) provideIntegerChecker() { return [ ['expected', 'input data'], [true, 1], [false, 'string'], ]; }

Slide 47

Slide 47 text

撰寫單元測試 /** @dataProvider provideIntegerChecker */ testIntegerChecker($expected, $input) provideIntegerChecker() { return [ ['expected', 'input data'], [true, 1], [false, 'string'], ]; }

Slide 48

Slide 48 text

撰寫單元測試  除了輸入錯誤的資料 還需要檢查程式是否會如預期傳回錯誤訊息  測試程式的是否會丟 exception  在刻意發生錯誤,程式是否會因為 error / warning 中 斷

Slide 49

Slide 49 text

撰寫單元測試 預期測試時會遇到 exception $this->expectException( InvalidArgumentException::class );

Slide 50

Slide 50 text

撰寫單元測試 預期測試時會遇到 error $this->expectWarning(); // Warning: Division by zero $a = 2 / 0;

Slide 51

Slide 51 text

撰寫單元測試  Fixtures  建立待測對象 (data structure / object) 並初始化  測試對象需要在每個 test case 執行前 reset  測試開始前給定測試用資料  降低不同 test case 互相影響 ( 依賴 ) 產生的問題

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

撰寫單元測試 public function setUp() { $this->stack = new SplStack(); $this->stack->push(1); $this->stack->push(2); } public function testStack() { $this->stack->count(); // int(2) }

Slide 54

Slide 54 text

撰寫單元測試 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 -----

Slide 55

Slide 55 text

撰寫單元測試  會用到 fixture 的情形  受測對象中不同的狀態 (state) 例如: NEW, DRAFT, PUBLISHED  Test case 執行後會導致受測對象狀態異動  受測對象需要初始化 或是要事先給定資料

Slide 56

Slide 56 text

Private Method / Property  使用 Reflection 來更改 visibility

Slide 57

Slide 57 text

Private Method / Variable class User { private height = 160; } $c = new ReflectionClass($user); $p = $c->getProperty('height'); $p->setAccessible(true); $this->assertEquals(160, $p->getValue());

Slide 58

Slide 58 text

Private Method / Property  Private method / property 不需另外測試  另一派的解釋  一般情況下會被其他 public method 呼叫  public method 所有 condition 走完一次 也表示 private method 也被執行過一遍 ( 高內聚 )

Slide 59

Slide 59 text

Test Doubles  doubles => 替身  減少依賴關係  取代真實的物件已便控制變因

Slide 60

Slide 60 text

Test Doubles  Dummy Object  Fake Object  Stub  Mock

Slide 61

Slide 61 text

Dummy Object  應付特定需求 (data type ... etc.)  對測試沒什麼影響 class DummyUser extends User {} $user = new DummyUser(); checkUserInfo(User $user);

Slide 62

Slide 62 text

Fake Object  行為相同  不影響正式環境 $pdo = new PDO('sqlite::memory:'); class FakeMemcache { .... }

Slide 63

Slide 63 text

Stub  固定的動作、行為 $stub = $this->createStub(User::class); $stub->method('height')- >willReturn('160'); $stub->method('weight')->willReturn('60'); $bmi = new Bmi($stub); $this->assertEquals(23.4375, $bmi- >count())

Slide 64

Slide 64 text

Mock  會驗證與其他物件的互動行為 $mock = $this->createMock(User::class); $mock->expects($this->once()) ->method('height') ->willReturn('160');

Slide 65

Slide 65 text

Test Doubles  功能越簡單越好  不要為了 test doubles 再寫 unit test class FakeMemcacahe { function get($key) {}; function set($key, $val) {}; function keys() {}; ..... }

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

單元測試 (Unit Test)  並非所有的 code 都有辦法建立 test case  Unit test 通過只能表示各項小功能正確 無法代表整個專案可以正確執行  靠整合測試來檢查整體行為是否如預期

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

撰寫可測試的程式

Slide 70

Slide 70 text

過於複雜的 constructor  Constructor 指負責資料初始化  不要與其他 class 、資料掛勾 function __construct() { try { $this->user = Order::last()->UID(); } catch (Exception $e) { throw $e; } }

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Conditions / Loops  巢狀 conditions / loops if ($id = 'xxx') { if ($password = 'xxx') { return true; } else { return false; } } else { return false; } 4 個測 試

Slide 73

Slide 73 text

Conditions / Loops 複雜度為 O(n^m)

Slide 74

Slide 74 text

Conditions / Loops  使用 short-circuit 改寫 if ($id != 'xxx') { return false; } if ($password != 'xxx') { return false; } // login successed 提早跳脫

Slide 75

Slide 75 text

Globals  可以使用全域變數,但有 side effect 較難測試 function isUserValid() { global $uid; $list = getAllUser(); return in_array($uid, $list); } 會改到 $uid ?

Slide 76

Slide 76 text

Globals function getUserInfo($uid) { $user = User::find($uid); $_POST['user'] = $user; } 麥來亂 XD

Slide 77

Slide 77 text

Singleton  好的 pattern ,但無法被測試  new 出來的物件,會被儲存在: private static variable 中

Slide 78

Slide 78 text

Singleton  好的 pattern ,但無法被測試  new 出來的物件,會被處存在: private static variable 中 要改 visibility

Slide 79

Slide 79 text

Singleton  好的 pattern ,但無法被測試  new 出來的物件,會被處存在: private static variable 中 無法 mock

Slide 80

Slide 80 text

SOLID Principles in OOP  Single responsibility  Open-closed  Liskov Substitution  Interface Segregation  Dependency Inversion

Slide 81

Slide 81 text

Single responsibility  單一職責原則  物件指處理單一類型的工作  高內聚

Slide 82

Slide 82 text

Single responsibility Class User { function getUserById(); function getUserByName(); function createUser(); } 僅處理 user

Slide 83

Slide 83 text

Single responsibility Class User { function getUserById(); function getUserByName(); function createUser(); function getUserByOrderId(); } 歪了 可能要建立 stub

Slide 84

Slide 84 text

Open-closed  類別應該是可以、且容易擴充功能的  類別對外部使用應該保持封閉 並透過 interface 規範操作方法

Slide 85

Slide 85 text

Open-closed class User { protected $users = []; } class UserList extends User implements Traversable { protected $currentIndex = 0; function current(); function next(); ....

Slide 86

Slide 86 text

Open-closed class TaskQueue { public $tasks; function pop(); function push(); } $queue->$task[] = new Task();

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Open-closed class TaskQueue { private $tasks; function pop(); function push(); } $queue->push(new Task()); 透過 interface 操作

Slide 89

Slide 89 text

Liskov Substitution  繼承相同父類別的物件,應該要有相同的行為  input / output 資料應相同  資料處理的行為應相同

Slide 90

Slide 90 text

Liskov Substitution class Animal {} class Food {} class DogFood extends Food {} class CatFood extends Food {}

Slide 91

Slide 91 text

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();

Slide 92

Slide 92 text

Liskov Substitution $dog = new Dog(); $cat = new Cat(); try { $dog->eat($food); $cat->eat($food); } catch (DogFoodException $de) { ... } } catch (CatFoodException $ce) { ... }

Slide 93

Slide 93 text

Liskov Substitution class Animal { public function eat (Food $food) {} } class Dog extends Animal { public function eat($food) { if (!$food instanceof DogFood) { parent::eat(); } }

Slide 94

Slide 94 text

Liskov Substitution  維持相同的行為、 interface  不需要多餘的判斷  caller 較容易測試

Slide 95

Slide 95 text

Interface Segregation  不要定義不會使用的 interface  建立多個精確、簡單的 interface

Slide 96

Slide 96 text

Interface Segregation  不要定義不會使用的 interface  建立多個精確、簡單的 interface

Slide 97

Slide 97 text

Interface Segregation interface DogInterface() { function walk(); function sleep(); }

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

Interface Segregation class RobotDog implements DogInterface { public function walk() { return 'walking ...'; } public function sleep() { throw new Exception(); } }

Slide 100

Slide 100 text

Interface Segregation interface walkable () { function walk(); } interface sleepable () { function sleep(); }

Slide 101

Slide 101 text

Interface Segregation class Dog implements walkable, sleepable { } class RobotDog implements walkable { }

Slide 102

Slide 102 text

Dependency Inversion  將依賴關係建立在抽象層,而非實體層 (abstraction) (concretion)  解耦合、降低依賴關係 (decouple)  和 dependency injection 是不同的東西

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

Dependency Inversion class User { function getByFile(File $reader) {} function getByAws(AwsS3 $reader) {} function getByDb(MySQL $reader) {} } User 類別需要知道各種 reader 的實作方 式

Slide 105

Slide 105 text

Dependency Inversion User File AWS S3 MySQL User 與 3 個類別相依

Slide 106

Slide 106 text

Dependency Inversion Interface ReaderInterface { function open(); function read(); function save(); } class File implements ReaderInterface; class AwsS3 implements ReaderInterface; class MySQL implements ReaderInterface;

Slide 107

Slide 107 text

Dependency Inversion class User { function get(ReaderInterface $reader){ $reader->open(); $users = $reader->read(); } }

Slide 108

Slide 108 text

Dependency Inversion User File AWS S3 MySQL ReaderInterface 每個類別只和 1 個 interface 有相依關係

Slide 109

Slide 109 text

Dependency Inversion  僅相依在 interface ,容易 mock  上層類別 (User) 不需考慮底層類別 (reader) 如何實作  底層類別改變實作方式不影響上層類別

Slide 110

Slide 110 text

Dependency Injection  一種 clean code 的實作方法  向外部要資料,而非自己找資料

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

Dependency Injection class User { function get(UserReader $reader) { $users = $reader->read(); } } 請外部提供資料、方法

Slide 114

Slide 114 text

Dependency Injection  自己實作需要承擔維護、修改成本  較容易寫測試 $mock = $this->createMock(); $mock->expect('read')- >willReturn([...]); $user->get($mock);

Slide 115

Slide 115 text

Q & A

Slide 116

Slide 116 text

Thanks for your attention