$30 off During Our Annual Pro Sale. View Details »

Effective Unit Testing

Effective Unit Testing

KKBOX 內訓簡報

大澤木小鐵

March 16, 2017
Tweet

More Decks by 大澤木小鐵

Other Decks in Programming

Transcript

  1. 有效的單元測試
    Jace Ju @ KKBOX
    https://commons.wikimedia.org/

    View Slide

  2. 一直以來,我們寫的測試就像…

    View Slide

  3. View Slide

  4. 捫心自問,你寫的測試有用嗎?

    View Slide

  5. 如果你常有這些抱怨…
    測試容易因為執行環境問題而失敗
    同事說看不懂你在測試什麼
    新增或修改測試要做的事很多
    常常沒有測試到真正重要的部份

    View Slide

  6. 你就需要瞭解測試的…
    測試替身 可讀性 可維護性 可靠性

    View Slide

  7. 測試替身
    Test Doubles
    https://www.pinterest.com

    View Slide

  8. 待測程式碼
    真實物件
    測試替身 測試替身
    測試替身

    View Slide

  9. 替身的作用
    隔離待測
    程式
    加速測試
    執行
    讓測試結
    果穩定
    模擬特殊
    狀況
    曝露隱藏
    的行為

    View Slide

  10. 替身的類型
    Stub
    Fake
    Spy
    Mock

    View Slide

  11. View Slide

  12. Stub
    繼承或實作相依類別與介面
    寫死的回傳值 (包含 Exception)
    Extract And Override

    View Slide

  13. 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 範例

    View Slide

  14. Fake
    提供輕量級實作
    執行速度很快
    建立成本較低

    View Slide

  15. 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 範例

    View Slide












  16. Laravel 中的應用

    View Slide

  17. Spy
    在相依物件上加入 spy 程式
    記錄相依物件的方法有無被呼叫
    事後透過 spy 物件的方法驗證

    View Slide

  18. $logger = Mockery::spy(Logger::class);
    $auth = new Auth($logger);
    $auth->login('user', 'pass');
    $logger->shouldHaveReceived('log')
    ->once();
    Spy 範例

    View Slide

  19. Mock
    進階版的 Stub 和 Spy
    讓不同輸入參數對應不同輸出
    精準控制方法的被呼叫次數

    View Slide

  20. $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 範例

    View Slide

  21. Spy -
    事後驗證行為 Mock - 事前定義
    對應輸出

    View Slide

  22. 替身反模式
    • 所有相依類別都用替身隔離
    假測試
    • 重構時也必須去修改測試中的替身
    過度的實作耦合
    • 資料庫邏輯被徹底隔離
    設計過當
    https://speakerdeck.com/adamwathan/tdd-the-good-parts-1

    View Slide

  23. 替身反模式解法
    • 可控制的部份就別用替身
    使用真實協作物件
    • 從抽象層次去驗證需求
    別驗證細節
    • 測試時改用 sqlite::memory:
    使用 ORM

    View Slide

  24. 替身就是造假的藝術
    https://zh.wikipedia.org/

    View Slide

  25. 可讀性
    Readability
    http://uxmastery.com/

    View Slide

  26. 可讀性原則
    別讓閱讀者思考
    一個測試只測試一個情境

    View Slide

  27. 可讀性反模式
    斷言不直覺
    斷言範圍太廣
    一次測太多事
    過份保護

    View Slide

  28. 斷言不直覺
    $this->assertTrue(array_key_exists('foo', $ary));
    $this->assertEquals(3, strpos('foobar', 'bar'));

    View Slide

  29. 用更直覺的斷言
    $this->assertArrayHasKey('foo', $ary);
    $this->assertContains('bar', 'foobar');

    View Slide

  30. /** @expectedException \Exception */
    public function textServiceShouldThrowExceptionWhenSomethingWrong()
    {
    // ...
    $service = new UserService();
    $service->getVerifiedUsers();
    }
    斷言範圍太廣

    View Slide

  31. /**
    * @expectedException \Illuminate\Database\QueryException
    */
    public function textThrowExceptionWhenQueryFailed()
    {
    // ...
    $service = new UserService();
    $service->getVerifiedUsers();
    }
    /**
    * @expectedException \Predis\Connection\ConnectionException
    */
    public function textThrowExceptionWhenRedisIsDisconnect()
    {
    // ...
    $service = new UserService();
    $service->getVerifiedUsers();
    }
    縮小斷言範圍

    View Slide

  32. 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']);
    }
    一次測太多事

    View Slide

  33. 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);
    }
    一次只測一件事

    View Slide

  34. public function testGetAlbumInfoSuccess()
    {
    $albumId = 10001;
    $expected = [
    $albumId => [
    'id' => '10001',
    'url' => '10001/300x300.jpg',
    ],
    ];
    $actual = $this->wrapper->getAlbumInfos([$albumId], 'tw');
    $this->assertEquals($expected, $actual);
    }
    一次只測一件事 (續)

    View Slide

  35. $users = (new UserRepository())->fetchVerifiedUsers();
    $this->assertNotNull($users);
    $this->assertCount(5, $users);
    過度保護

    View Slide

  36. $users = (new UserRepository())->fetchVerifiedUsers();
    $this->assertNotNull($users);
    $this->assertCount(5, $users);
    刪除不必要的斷言

    View Slide

  37. 可維護性
    Maintainability
    http://soniasaelensdeco.canalblog.com/

    View Slide

  38. 可維護性原則
    不論誰接手都能運作
    減少不穩定的狀況

    View Slide

  39. 可維護性反模式
    條件邏輯
    重複的程式碼
    相依的測試
    脆弱的測試

    View Slide

  40. $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']);
    }
    }
    條件邏輯

    View Slide

  41. 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']);
    }
    重複的程式碼

    View Slide

  42. 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

    View Slide

  43. 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 (續)

    View Slide

  44. 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());
    }
    }
    相依的測試

    View Slide

  45. 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());
    }
    }
    獨立每個測試案例

    View Slide

  46. 脆弱的測試
    不可控的執行環境
    寫死的檔案路徑
    緩慢的等待 (Sleep)

    View Slide

  47. 可靠性
    Reliability

    View Slide

  48. 可靠性原則
    去除任何的疑惑
    要真正是可信賴的

    View Slide

  49. 可靠性反模式
    被註解的測試
    永不失敗的測試
    虛假的測試
    有條件的測試

    View Slide

  50. 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());
    }
    }
    被註解的測試

    View Slide

  51. class ImageApiWrapperTest extends TestCase
    {
    public function testDevelopmentMode()
    {
    $dev = true;
    $app = new ImageApiWrapper($dev);
    }
    }
    永不失敗的測試

    View Slide

  52. /** @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);
    }
    虛假的測試

    View Slide

  53. 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());
    }
    }
    有條件的測試

    View Slide

  54. 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());
    }
    用斷言取代條件式

    View Slide

  55. 總結
    Summary
    https://www.bankinghub.eu/

    View Slide

  56. 驗證需求而不是驗證細節。
    一次只測試一件事。
    要有清楚的斷言。
    只隔離不可控的因素。

    View Slide

  57. View Slide

  58. http://newsletters.getresponse.com/

    View Slide