Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Effective Unit Testing
Search
大澤木小鐵
March 16, 2017
Programming
660
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Effective Unit Testing
KKBOX 內訓簡報
大澤木小鐵
March 16, 2017
More Decks by 大澤木小鐵
See All by 大澤木小鐵
JSConf Asia 2014 Sessions
jaceju
4
460
What happens in Laravel 4 bootstraping
jaceju
9
610
Deal with Laravel assets by Bower & Gulp
jaceju
30
2.1k
Leaning MVC By Example
jaceju
0
440
ng-conf_2014
jaceju
2
1.1k
The Power of JavaScript in JSConf.Asia 2013
jaceju
5
460
jQuery vs AngularJS, dochi?
jaceju
20
3.1k
Begining Composer
jaceju
24
5.5k
Checkup your web pages
jaceju
44
3.2k
Other Decks in Programming
See All in Programming
AI時代に設計が 最大の生産性レバーになる 意図駆動開発とデータを消さない設計|Don't Delete Your Data or Your Intent — Design as the Deepest Lever in the AI Era
tomohisa
1
730
【SRE NEXT 2026 Lunch Session】一人目専任SREの立ち上げを加速する ― AIと進めたオンボーディングで2分を0.04秒にした話
pkshadeck
PRO
0
3.5k
【QA Test Talk Vol.8】AI-DLC による Whole Team Approach の加速
pkshadeck
PRO
0
120
ここ半年くらいでAIに作らせたR用ツール
eitsupi
0
370
AWS CDK を「作」ってみた 〜フルスクラッチで見えた CDK の裏側〜 / aws-cdk-from-scratch
gotok365
3
2.8k
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
540
OpenSpecのproposalにbrainstormingを持たせてみた
tigertora7571
1
210
仕様書を書く前にハーネスを作る - Agent Native開発は「探索を速く、判定を固く」
gotalab555
4
1.6k
Detecting Compromised CI with eBPF and Cilium Tetragon
lizrice
0
160
FDEが実現するAI駆動経営の現在地
gonta
2
270
AI Engineeringは、AIプロダクトだけのものか? 〜AIがソフトウェアを作る時代の新しい当たり前〜 / No AI in your product. AI Engineering in your development.
rkaga
4
380
自動化したのに回らないテスト運用の壁ーAI時代の品質責任と生産性
mfunaki
0
130
Featured
See All Featured
Raft: Consensus for Rubyists
vanstee
141
7.6k
Darren the Foodie - Storyboard
khoart
PRO
3
3.5k
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
440
Designing for Timeless Needs
cassininazir
1
430
Building Flexible Design Systems
yeseniaperezcruz
330
40k
The Director’s Chair: Orchestrating AI for Truly Effective Learning
tmiket
1
260
Introduction to Domain-Driven Design and Collaborative software design
baasie
1
930
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
460
What's in a price? How to price your products and services
michaelherold
247
13k
State of Search Keynote: SEO is Dead Long Live SEO
ryanjones
0
240
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
210
Transcript
有效的單元測試 Jace Ju @ KKBOX https://commons.wikimedia.org/
一直以來,我們寫的測試就像…
None
捫心自問,你寫的測試有用嗎?
如果你常有這些抱怨… 測試容易因為執行環境問題而失敗 同事說看不懂你在測試什麼 新增或修改測試要做的事很多 常常沒有測試到真正重要的部份
你就需要瞭解測試的… 測試替身 可讀性 可維護性 可靠性
測試替身 Test Doubles https://www.pinterest.com
待測程式碼 真實物件 測試替身 測試替身 測試替身
替身的作用 隔離待測 程式 加速測試 執行 讓測試結 果穩定 模擬特殊 狀況 曝露隱藏
的行為
替身的類型 Stub Fake Spy Mock
None
Stub 繼承或實作相依類別與介面 寫死的回傳值 (包含 Exception) Extract And Override
class StubRepository implements RepositoryInterface { public function findUser(int $id): User
{ return new StubUser([ 'id' => 1 ]); } public function fetchUsers(array $ids): Collection { throw new \InvalidArgumentException(); } } Stub 範例
Fake 提供輕量級實作 執行速度很快 建立成本較低
class FakeSession implements SessionInterface { private $storage = []; public
function put($name, $value) { $this->storage[$name] = $value; } public function get($name) { return $this->storage[$name] ?? null; } } Fake 範例
<?xml version="1.0" encoding="UTF-8"?> <phpunit> <php> <env name="APP_ENV" value="testing"/> <env name="CACHE_DRIVER"
value="array"/> <env name="SESSION_DRIVER" value="array"/> <env name="QUEUE_DRIVER" value="sync"/> <env name="DB_DRIVER" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/> </php> </phpunit> Laravel 中的應用
Spy 在相依物件上加入 spy 程式 記錄相依物件的方法有無被呼叫 事後透過 spy 物件的方法驗證
$logger = Mockery::spy(Logger::class); $auth = new Auth($logger); $auth->login('user', 'pass'); $logger->shouldHaveReceived('log')
->once(); Spy 範例
Mock 進階版的 Stub 和 Spy 讓不同輸入參數對應不同輸出 精準控制方法的被呼叫次數
$service = Mockery::mock(UserService::class); $service->shouldReceive('authenticate') ->with('jaceju', '123456') ->andReturn(false); $service->shouldReceive('authenticate') ->with('butany', '123456')
->andReturn(true); $userController = new UserController($service); $actual = $userController->login('jaceju', '123456'); $this->assertFalse($actual); $actual = $userController->login('butany', '123456'); $this->assertTrue($actual); Mock 範例
Spy - 事後驗證行為 Mock - 事前定義 對應輸出
替身反模式 • 所有相依類別都用替身隔離 假測試 • 重構時也必須去修改測試中的替身 過度的實作耦合 • 資料庫邏輯被徹底隔離 設計過當
https://speakerdeck.com/adamwathan/tdd-the-good-parts-1
替身反模式解法 • 可控制的部份就別用替身 使用真實協作物件 • 從抽象層次去驗證需求 別驗證細節 • 測試時改用 sqlite::memory:
使用 ORM
替身就是造假的藝術 https://zh.wikipedia.org/
可讀性 Readability http://uxmastery.com/
可讀性原則 別讓閱讀者思考 一個測試只測試一個情境
可讀性反模式 斷言不直覺 斷言範圍太廣 一次測太多事 過份保護
斷言不直覺 $this->assertTrue(array_key_exists('foo', $ary)); $this->assertEquals(3, strpos('foobar', 'bar'));
用更直覺的斷言 $this->assertArrayHasKey('foo', $ary); $this->assertContains('bar', 'foobar');
/** @expectedException \Exception */ public function textServiceShouldThrowExceptionWhenSomethingWrong() { // ...
$service = new UserService(); $service->getVerifiedUsers(); } 斷言範圍太廣
/** * @expectedException \Illuminate\Database\QueryException */ public function textThrowExceptionWhenQueryFailed() { //
... $service = new UserService(); $service->getVerifiedUsers(); } /** * @expectedException \Predis\Connection\ConnectionException */ public function textThrowExceptionWhenRedisIsDisconnect() { // ... $service = new UserService(); $service->getVerifiedUsers(); } 縮小斷言範圍
public function testAlbum() { $albumId = 10001; $albumIds = [$albumId];
$wrapper = new ImageApiWrapper(); $actual = $wrapper->getAlbumUrls($albumIds); $this->assertEquals('10001/300x300.jpg', $actual[$albumId]); $actual = $wrapper->getAlbumInfos($albumIds); $this->assertEquals('10001/300x300.jpg', $actual[$albumId]['url']); } 一次測太多事
protected function setUp() { $this->wrapper = new ImageApiWrapper(); } public
function testGetAlbumUrlsSuccess() { $albumId = 10001; $expected = [ $albumId => '10001/300x300.jpg', ]; $actual = $this->wrapper->getAlbumUrls([$albumId]); $this->assertEquals($expected, $actual); } 一次只測一件事
public function testGetAlbumInfoSuccess() { $albumId = 10001; $expected = [
$albumId => [ 'id' => '10001', 'url' => '10001/300x300.jpg', ], ]; $actual = $this->wrapper->getAlbumInfos([$albumId], 'tw'); $this->assertEquals($expected, $actual); } 一次只測一件事 (續)
$users = (new UserRepository())->fetchVerifiedUsers(); $this->assertNotNull($users); $this->assertCount(5, $users); 過度保護
$users = (new UserRepository())->fetchVerifiedUsers(); $this->assertNotNull($users); $this->assertCount(5, $users); 刪除不必要的斷言
可維護性 Maintainability http://soniasaelensdeco.canalblog.com/
可維護性原則 不論誰接手都能運作 減少不穩定的狀況
可維護性反模式 條件邏輯 重複的程式碼 相依的測試 脆弱的測試
$service = new UserService(); $verifiedUsers = $service->getVerifiedUsers(); foreach ($verifiedUsers as
$index => $verifiedUser) { if ($index === 0) { $this->assertEquals('butany', $verifiedUser['name']); } if ($index === 1) { $this->assertEquals('jaceju', $verifiedUser['name']); } } 條件邏輯
public function testUserServiceShouldReturnExpectedVerifiedUsers() { $service = new UserService(); $verifiedUsers =
$service->getVerifiedUsers(); $this->assertCount(2, $verifiedUsers); $this->assertArrayHasKey('name', $verifiedUsers[0]); $this->assertEquals('butany', $verifiedUsers[0]['name']); $this->assertArrayHasKey('name', $verifiedUsers[1]); $this->assertEquals('jaceju', $verifiedUsers[1]['name']); } 重複的程式碼
class UserServiceTest { use AssertionHelper; private $service; public function setUp()
{ $this->service = new UserService(); } } trait AssertionHelper { private function assertArrayHasKeyWithValue($array, $key, $value) { $this->assertArrayHasKey($key, $array); $this->assertEquals($value, $array[$key]); } } 利用 setUp 和 Trait
public function testUserServiceShouldReturnExpectedVerifiedUsers() { $verifiedUsers = $this->service->getVerifiedUsers(); $this->assertCount(2, $verifiedUsers); $this->assertArrayHasKeyWithValue($verifiedUsers[0],
'name', 'butany'); $this->assertArrayHasKeyWithValue($verifiedUsers[1], 'name', 'jaceju'); } 利用 setUp 和 Trait (續)
class ConfigTest extends TestCase { private $config; public function testAddConfigSuccess()
{ $configFile = __DIR__ . '/fixture/config.json'; $this->config = new Config($configFile); $this->config->add('debug', true); $this->assertObjectHasAttribute('debug', $this->config); } public function testInjectConfigToAppShouldWork() { $app = new App($this->config); assertThat($app->debugMode, isTrue()); } } 相依的測試
class AppTest extends TestCase { public function testInjectConfigToAppShouldWork() { $config
= Mockery::mock(Config::class); $config->shouldRecive('toArray') ->andReturn(['debug' => true]); $app = new App($config); assertThat($app->debugMode, isTrue()); } } 獨立每個測試案例
脆弱的測試 不可控的執行環境 寫死的檔案路徑 緩慢的等待 (Sleep)
可靠性 Reliability
可靠性原則 去除任何的疑惑 要真正是可信賴的
可靠性反模式 被註解的測試 永不失敗的測試 虛假的測試 有條件的測試
class AppTest extends TestCase { public function testInjectConfigToAppShouldWork() { //
$config = Mockery::mock(Config::class); // $config->shouldRecive('toArray') // ->andReturn(['debug' => true]); // $app = new App($config); // assertThat($app->debugMode, isTrue()); } } 被註解的測試
class ImageApiWrapperTest extends TestCase { public function testDevelopmentMode() { $dev
= true; $app = new ImageApiWrapper($dev); } } 永不失敗的測試
/** @test */ public function it_should_get_empty_array_when_get_urls_with_invalid_id() { $albumId = 'invalid_id';
$albumIds = [$albumId]; $wrapper = new ImageApiWrapper(); $actual = $wrapper->getAlbumUrls($albumIds); $expected = [ $albumId => 'noimg/300x300.jpg', ]; $this->assertEquals($expected, $actual); } 虛假的測試
public function testRunListCommandAtHomeDirectory() { $expectedFile = 'hello.txt'; $targetDirectory = '/home/jaceju';
$this->createFile($expectedFile, $targetDirectory); $command = new ListCommand($targetDirectory); $process = Process::run($command); if ($process->getExitCode() === 0) { $this->assertEquals($expectedFile, $process->getOutput()); } } 有條件的測試
public function testRunListCommandAtHomeDirectoryShouldGetExpectedFile() { $expectedFile = 'hello.txt'; $targetDirectory = '/home/jaceju';
$this->createFile($expectedFile, $targetDirectory); $command = new ListCommand($targetDirectory); $process = Process::run($command); $this->assertEquals(0, $process->getExitCode()); $this->assertEquals($expectedFile, $process->getOutput()); } 用斷言取代條件式
總結 Summary https://www.bankinghub.eu/
驗證需求而不是驗證細節。 一次只測試一件事。 要有清楚的斷言。 只隔離不可控的因素。
None
http://newsletters.getresponse.com/