13年続くレガシーサービスを安全にリリースし続けるためのテスト戦略 / rakus-meetup-osaka-vol8-2020-08-05

Dd672f13e7e17714700544cb355cbaba?s=47 kyoshimoto
August 05, 2020

13年続くレガシーサービスを安全にリリースし続けるためのテスト戦略 / rakus-meetup-osaka-vol8-2020-08-05

Rakus Meetup Osaka #8 の発表資料です。
https://rakus.connpass.com/event/161744/

Dd672f13e7e17714700544cb355cbaba?s=128

kyoshimoto

August 05, 2020
Tweet

Transcript

  1. 13年続くレガシーサービスを安全に リリースし続けるためのテスト戦略 RAKUS Meetup Osaka #8 株式会社ラクス 吉元和仁

  2. 自己紹介 • 所属・氏名 • 株式会社ラクス 第四開発部 • 吉元 和仁(よしもとかずひと) •

    仕事 • 2018年10月より自社サービス「配配メール」の開発担当
  3. 配配メール について • BtoB向けメールマーケティング・サービス • 中小企業のお客様の多種多様な業務に合わせて4つのプラン、 8つのオプション機能を提供

  4. 2019年10月に新プラン 「配配メールBridge」をリリース

  5. 話すこと • 13年続く自動テストの無いウェブサービスで、レガシーと向 き合い、安全にリリースし続けるためのテスト戦略とテスト ツールの活用事例を紹介。

  6. レガシーサービスの定義

  7. レガシーサービスの定義 • テストがないコードはレガシーコード だ! (書籍「レガシーコード 改善ガイド」より)

  8. レガシーサービスの定義 • テストされていない/テストできないコード • 柔軟性のないコード • 技術的負債を抱え込んでいるコード (書籍「レガシーソフトウェア改善ガイド」より)

  9. レガシーシステムの改善方針 • リファクタリング • リアーキテクティング • ビッグ・リライト (書籍「レガシーソフトウェア改善ガイド」より)

  10. 我々のレガシーサービス

  11. 我々のレガシーサービス • 2007年5月にサービス提供を開始 • 長年の機能追加により、ユーザへの価値提供を増大 • 一方で、コードの複雑性も増大

  12. メソッドが組み合わせ爆発 • プログラム修正がコアロジックに集中 • 増え過ぎた引数 • 引数の論理演算($midOrSid) • No グローバル変数,

    No life (https://youtu.be/Q4gTV4r0zRs)
  13. BigQuery生成ロジック • 複数のメソッドが1つのSQLを生成するロジック • サービスの成長とともにSQLが巨大化 • 各メソッドは責務崩壊 • つじつま合わせのハッキングコード •

    新人泣かせのBigQuery(SQL)生成ロジックへと成長
  14. リファクタリングしたい

  15. レガシーコードのジレンマ • リファクタリングを行うとき、最初にすることは常に同じで す。対象となるコードについてきちんとしたテスト群を作り 上げることです。テストは不可欠です。 (書籍「リファクタリング」より)

  16. レガシーコードのジレンマ • 自動テストの仕組みも文化もないサービス • ドキュメントが無く、ソースコードが仕様 • テストを作るためには、まずはじめに複雑度の高いロジック を理解することから始めなければならない

  17. 開発チームの課題

  18. 開発チームの課題 • スター選手は新規サービス開発の最前線へ • 相次ぐコアメンバーの異動 • 開発チームの半数以上は若手エンジニア

  19. 開発チームの課題 • ソースコードが仕様 • 既存仕様の考慮漏れにより、開発終盤で設計が破綻 • 進捗90%の壁 • 納期のプレッシャーにより品質が低下 •

    疲弊する開発チーム
  20. 計画の不確実性をなくしたい

  21. 計画の不確実性 • 楽観主義などの要因により、コストを低く見積もる傾向があ る。 書籍「人月の神話」より

  22. 計画の不確実性 • 見積りは、あくまで予測です。 • 予測を「ノルマ」にした途端、それを達成するための過負荷 な労働が生まれ、クオリティが下がってしまいます。 • 書籍「エンジニア組織論への招待」より

  23. 計画の不確実性 • エンジニアリングで重要なのは「どうしたら効率よく不確実 性を減らしていけるのか」という考え方なのです。 • 書籍「エンジニア組織論への招待」より

  24. 課題まとめ • 課題1 • 複雑度が高すぎるプロダクトコード • 課題2 • 計画の不確実性の問題

  25. レガシーサービスを安全にリ リースし続けるための テスト戦略

  26. 課題まとめ(再掲) • 課題1 • 複雑度が高すぎるプロダクトコード • 課題2 • 計画の不確実性の問題

  27. テストの7原則 1. テストは欠陥があることは示せるが、欠陥がないことは示せない 2. 全数テストは不可能 3. 早期テストで時間とコストを節約 4. 欠陥の偏在 5.

    殺虫剤のパラドックスにご用心 6. テストは状況次第 7. 「バグゼロ」の落とし穴 (参考: http://jstqb.jp/dl/JSTQB-SyllabusFoundation_Version2018V31.J02.pdf)
  28. テストの7原則 4. 欠陥の偏在 リリース前のテストで見つかる欠陥や運用時の故障の大部分は、 特定の少数モジュールに集中する。 テストの労力を集中させるために欠陥の偏在を予測し、テストや 運用での実際の観察結果に基づいてリスク分析を行う。

  29. 戦略① 安全にリリースし続ける ための重要機能テスト

  30. 重要機能テストとは • 我々の開発チームでは、代替手段がなく顧客影響が大きいバ グをクリティカルバグと呼ぶ。 • このバグを抽出するためのテストが重要機能テスト。 • 安全なリリースサイクルを回すためには、重要機能テストの 実施が不可欠。

  31. テストの作成手順 1. サービスの有識者と協議の上で重要機能を抽出 2. 重要機能のテストケースを作成 3. テストを手動実行し、カバレッジ結果を取得 4. カバレッジ結果を参考に、テストケースを見直し

  32. テストケースの作成手順 1. サービスの有識者と協議の上で重要機能を抽出 2. 重要機能のテストケースを作成 3. テストを手動実行し、カバレッジ結果を取得 4. カバレッジ結果を参考に、テストケースを見直し

  33. 手動テストのカバレッジって 取得できるの?

  34. 手動テストの カバレッジ取得について

  35. 利用するライブラリ • Xdebug (https://xdebug.org/ ) • PHP_CodeCoverage (Github: https://github.com/sebastianbergmann/php-code-coverage)

  36. ライブラリのインストール • Xdebug • 公式ページ参照 • https://xdebug.org/docs/install • PHP_CodeCoverage composer

    require --dev phpunit/php-code-coverage
  37. カバレッジログ 出力プログラムの準備

  38. カバレッジログ出力プログラムを準備 • <?php • xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); • function output_coverage_report_file()

    { • if (!xdebug_code_coverage_started()) { • return; • } • xdebug_stop_code_coverage(false); • $jsonData = json_encode(xdebug_get_code_coverage()); • // コードカバレッジ結果をJSONファイルに出力する • $reportFile = __DIR__ . '/reports/json/coverage-' . microtime(true) . '.json'; • file_put_contents($reportFile, $jsonData); • } register_shutdown_function('output_coverage_report_file'); •
  39. カバレッジログ出力プログラムを準備 • <?php • xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); • • •

    • • • • • • • ① カバレッジ取得開始
  40. カバレッジログ出力プログラムを準備 • <?php • xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); • function output_coverage_report_file()

    { • if (!xdebug_code_coverage_started()) { • return; • } • xdebug_stop_code_coverage(false); • $jsonData = json_encode(xdebug_get_code_coverage()); • // コードカバレッジ結果をJSONファイルに出力する • $reportFile = __DIR__ . '/reports/json/coverage-' . microtime(true) . '.json'; • file_put_contents($reportFile, $jsonData); • } register_shutdown_function('output_coverage_report_file'); • ② プログラム終了時に実行する コールバック関数を定義
  41. カバレッジログ出力プログラムを準備 • <?php • xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); • function output_coverage_report_file()

    { • if (!xdebug_code_coverage_started()) { • return; • } • xdebug_stop_code_coverage(false); • $jsonData = json_encode(xdebug_get_code_coverage()); • // コードカバレッジ結果をJSONファイルに出力する • $reportFile = __DIR__ . '/reports/json/coverage-' . microtime(true) . '.json'; • file_put_contents($reportFile, $jsonData); • } register_shutdown_function('output_coverage_report_file'); • ③ カバレッジ取得停止
  42. カバレッジ結果出力プログラムを準備 • <?php • xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); • function output_coverage_report_file()

    { • if (!xdebug_code_coverage_started()) { • return; • } • xdebug_stop_code_coverage(false); • $jsonData = json_encode(xdebug_get_code_coverage()); • // コードカバレッジ結果をJSONファイルに出力する • $reportFile = __DIR__ . '/reports/json/coverage-' . microtime(true) . '.json'; • file_put_contents($reportFile, $jsonData); • } register_shutdown_function('output_coverage_report_file'); • ④ カバレッジデータを ファイル出力
  43. カバレッジレポート 出力プログラムを準備

  44. カバレッジレポート出力プログラムを準備 • <?php • ini_set('max_execution_time', 0); • ini_set('memory_limit', -1); require_once

    __DIR__ . "/vendor/autoload.php"; • $summaryCoverage = new SebastianBergmann¥CodeCoverage¥CodeCoverage(); • // ... 中略 ... • $summaryCoverage->filter() • ->addDirectoryToWhitelist('/usr/local/myapp', ['.php', '.inc’]); • $reportFiles = glob(__DIR__ . '/reports/json/*.json’); ① レポート用モジュールを生成し、ログデータ読み込み準備
  45. • $countFiles = count($reportFiles); • for ($i = 0; $i

    < $countFiles; $i++) { • printf("[%d/%d] append coverage data %s¥n", • ($i + 1), $countFiles, $reportFiles[$i] • ); • $tmpData = json_decode( • file_get_contents($reportFiles[$i]), JSON_OBJECT_AS_ARRAY); • $summaryCoverage->append($tmpData, 'myapp'); • } カバレッジレポート出力プログラムを準備 ② ログデータを読み込む
  46. • echo "Output html report start..." . PHP_EOL; • $report

    = new SebastianBergmann¥CodeCoverage¥Report¥Html¥Facade(); • $report->process($summaryCoverage, __DIR__ . '/reports/html'); • echo "Output html report completed" . PHP_EOL; カバレッジレポート出力プログラムを準備 ③ カバレッジ結果をHTML出力
  47. PHP設定ファイルの編集

  48. PHP設定ファイルの編集 • 「auto_prepend_file」ディレクティブ設定 ; Automatically add files before PHP document.

    ; http://php.net/auto-prepend-file auto_prepend_file = {カバレッジログ出力プログラムのパス}
  49. カバレッジレポート出力の流れ 1. カバレッジ用プログラムの配置 2. 「auto_prepend_file」設定し、サービス再起動 3. 重要機能テストを手動実行 4. 「auto_prepend_file」設定クリアし、サービス再起動 5.

    カバレッジ出力プログラムを実行
  50. カバレッジ取得&HTML出力

  51. カバレッジ取得&HTML出力

  52. カバレッジ取得&HTML出力 赤色行が重要機能ロジックであれば テストケースを追加する

  53. 重要機能テストの自動化

  54. 利用するライブラリ • Puppeteer • E2Eテスト用ライブラリ • プログラミング言語: JavaScript (https://github.com/puppeteer/puppeteer) •

    Visual Studio Code (IDE環境) • テストプログラム作成のためのIDE環境として利用 (https://azure.microsoft.com/ja-jp/products/visual-studio-code/)
  55. ライブラリのインストール • puppeteer • Visual Studio Code • 割愛 npm

    install puppeteer
  56. PageObject パターンでユーザ操作をメソッド化 module.exports = class HMPage { //...中略... async input件名(aSubject)

    { await this.page.type('#subject', aSubject); } async click新規作成() { await this.page.waitForXPath('//a[contains(text(), "新規作成")]') const link = await this.page.$x('//a[contains(text(), "新規作成")]') await link[0].click() await this.navigationPromise; } async click次へ() { await this.page.waitForXPath('//a[contains(text(), "次へ")]') const link = await this.page.$x('//a[contains(text(), "次へ")]') await link[0].click() await this.navigationPromise; }
  57. 重要機能テストをテストコード化 • (async () => { • const page =

    new HMPage() //.. 中略... await page.inputログインID("テスト太郎") • await page.inputパスワード("password") • await page.clickBtnログイン() • await page.click新規作成() • await page.click次へ() • actualValue = await page.getTitle() • assert.equal(actualValue, 'メール作成画面', '「メール作成画面」画面が表示される'); • await page.input件名("テストメール件名") • await page.input本文("テストメール本文") • await page.click次へ() • actualValue = await page.getTitle() • assert.equal(actualValue, 'メール確認画面', '「メール確認画面」画面が表示される');
  58. 重要機能テストを自動化

  59. 戦略② 不確実性に立ち向かうための テスト駆動開発

  60. 課題まとめ(再掲) • 課題1 • 複雑度が高すぎるプロダクトコード • 課題2 • 計画の不確実性の問題

  61. 安全にリリースし続けるために • 開発終盤の不確実性を低減させる必要がある

  62. テスト駆動開発

  63. テスト駆動開発について • 基本となる開発サイクル • 失敗するテストを書く • できる限り早く、テストに通るような最小限のコードを書く • コードの重複を除去する(リファクタリング)

  64. テスト駆動開発について • ソフトウェアテストの世界における本来のテストとは、認知 の外を探求する、いわば創造的破壊行為です。それにたいし てTDDのテストとは、いわばプログラミングや設計の補助線、 治具です。 (書籍「テスト駆動開発」より)

  65. 我々のテスト駆動開発

  66. 導入前 1. 既存ロジックの仕様を把握する 2. プログラム設計書を書く 3. 実装する 4. 修正ロジック、デグレードしそうなロジックを手動テスト する

  67. 導入後 1. 既存ロジックのユニットテスト作成 2. 既存ロジックをリファクタリング 3. 新規ロジックのユニットテスト 4. 実装して動くものを作る 5.

    リファクタリングする
  68. テスト駆動開発への期待 • 納期のプレッシャーを低減 • 開発終盤でも安心して、修正・再設計できる。 • 既存バグや複雑な作業に翻弄されずに、進捗させやすくなる。 • 開発タスクを分業しやすくし、クリティカルパスを低減できる。 •

    スイッチングコストの低減 • 実装結果がすぐにフィードバックされるので、実装に専念できる。 • 調査・実装・デバッグ・既存バグによる作業中断によるスイッチングコ ストを低減できる。 • 開発チームの設計力底上げ • テスト容易性を考慮した設計が行えるようになり、開発チームの設計力 を底上げできる。
  69. テスト駆動開発用ツール • PHPUnit • PHPで動くxUnit系フレームワーク (公式: https://phpunit.de/) • Guzzle •

    PHPで利用できるHTTPクライアント (http://docs.guzzlephp.org/en/stable/index.html)
  70. ライブラリのインストール • PHPUnit • Guzzle • PHPで動くHTTPクライアント composer require phpunit/phpunit

    composer require guzzlehttp/guzzle:^7.0
  71. 実行環境

  72. 結果報告 (中間報告)

  73. 重要機能テストについて • 重要機能テストにより、発見された不具合は今のところ0件 • 重要機能が定義され、死守すべき品質基準が明文化された点は大き い • 自動化テストについては、一部オプション機能を除き自動化済み • 現在は、より堅牢なサービスを目指し、重要機能の範囲を広げて自

    動テストを拡充中
  74. テスト駆動開発 • プロジェクト終盤の不確実性が圧倒的に低減できている • ユニットテストがあることで、開発終盤でも安心して設計方針を見 直せる • 携わった開発メンバー全員が、この取り組みを評価しており、今後 積極的に採用していく意思をしめしている

  75. リリース状況 • 2019年03月予定 v5.3リリース • 2019年04月予定 v5.3リリース • 2019年05月16日 v5.3リリース

  76. リリース状況 • 2019年10月10日 v6.0リリース • 2020年05月23日 v6.1リリース • 2020年04月16日 v6.2リリース

    • 2020年06月03日 v6.3リリース • 2020年08月末 v6.4リリース
  77. まとめ

  78. まとめ • 重要機能テストで品質基準を明文化し、E2Eテストで品質基 準を検査する。 • テスト駆動開発で品質を向上し、計画の不確実性を低減する。

  79. まとめ • 重要機能テストとテスト駆動開発で、レガシーシステムと向 き合うことで、安全なリリースを実現できた。

  80. ご静聴ありがとう ございました