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

テスト・設計研修【MIXI 24新卒技術研修】

テスト・設計研修【MIXI 24新卒技術研修】

本スライドは、MIXIの2024年度新卒向け技術研修で使用された資料です。

<科目名>
テスト・設計研修

<関連リンク>
動画▶ https://youtu.be/Z_W61VpaXKA

※お願い※ 〜 資料・動画・リポジトリのご利用について〜
公開している資料や動画は、是非、勉強会や社内の研修などにご自由にお使いいただければと思いますが、以下のような場でのご利用はご遠慮ください。
- 受講者から参加費や授業料など金銭を集めるような場での利用
(会場費や飲食費など勉強会の運営に必要な実費を集める場合は問題ありません)
- 出典を削除または改変しての利用

MIXI ENGINEERS

July 22, 2024
Tweet

Video

More Decks by MIXI ENGINEERS

Other Decks in Technology

Transcript

  1. 22 ©MIXI ⾃⼰紹介 • ⼤倉 真⼀希 (@maiki.okura) • 22新卒 •

    みてね事業部 プロダクト開発Mグループ ◦ 物理商品を扱うチーム ◦ server が多めでたまに iOS, Android など • 好きなもの ◦ フライドポテト ◦ 背脂ラーメン • 好きな分野 ◦ 成果‧⽣産性向上
  2. 33 ©MIXI ⾃⼰紹介 • 加藤 修悟 (@shugo.kato) a.k.a. ろぐみ •

    21新卒 • Vantage スタジオ Romi 事業部 開発グループ ◦ サーバ/フロント/インフラ ◦ なんでもやさん • 好きなもの ◦ キーボードづくり ◦ チャーシューづくり • 好きな分野 ◦ 爆速化
  3. 44 ©MIXI 本⽇の流れ 1. 講義 2. 演習1 (ペアプログラミング) ◦ 実装

    ◦ 各チーム同⼠でコードレビュー & 修正 3. 演習2、 3 … お昼は13:00頃〜を予定
  4. 66 ©MIXI 本⽇の流れ • テスト‧ソフトウェアテストとは ◦ ソフトウェアの品質の話 ◦ TDDの話 ◦

    テスタビリティの話 ◦ テスト技法の話 ◦ テストの7原則 • ペアプログラミング • コードレビューの仕⽅とされ⽅
  5. 15 15 ©MIXI テスト駆動開発(Test-Driven Development / TDD)とは? TDDとは、プログラミングのワークフローの⼀つ 以下のステップで開発を⾏う 1.

    テストリストを作成する a. 挙動の要件を書き出す 2. テストを書く (Red) a. 実装はないのでもちろんテストは落ちる 3. テストを通すために、実装をする (Green) a. まずはテストを通すことを考えてみる 4. リファクタリングする (Refactor) 5. テストリストが完了するまで2〜4を繰り返す Red Green Refactor 参考:Kent Beck 著 / 和田卓人 訳, テスト駆動開発, https://shop.ohmsha.co.jp/shopdetail/000000004967/ 和田卓人, 【翻訳】テスト駆動開発の定義, https://t-wada.hatenablog.jp/entry/canon-tdd-by-kent-beck
  6. 16 16 ©MIXI TDD に関するありがちな勘違い Q. テストコードを書くこと⾃体がTDD? A. それは⾃動テストです!  

    ⾃動テストがあるだけでもとても⽣産性が上がりますが TDD とは別です。 Q. テストを先に書いていればTDD? A. それはテストファーストです!   全テストを先に書くと仕様が把握できるなどのメリットがあります。   ⼀⽅で後戻りのリスクが上がったりします。
  7. 20 20 ©MIXI TDDの実例|1つめの要件のテストを実装 メソッドの本体がないのでテストが落ちる!! ImportError while importing test module

    '/app/tests/app/test_bark.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: ... from app.bark import bark E ImportError: cannot import name 'bark' from 'app.bark' (/app/app/bark.py)
  8. 23 23 ©MIXI TDDの実例|2つめの要件のテストを実装 以下のテスト assert を追加 ❏ 引数で猫(cat)を渡した時に、猫の鳴き声 ʻmeow!’を返す

    import pytest from app.bark import bark def test_bark(): assert bark() == 'bowwow!' assert bark('cat') == 'meow!' # 追加
  9. 24 24 ©MIXI TDDの実例|2つめの要件のテストのテストを実⾏ 元々のメソッドは引数を取っていないのでテストが落ちる E TypeError: bark() takes 0

    positional arguments but 1 was given tests/app/test_bark.py:6: TypeError ============================= short test summary info ======================================== FAILED tests/app/test_bark.py::test_bark - TypeError: bark() takes 0 positional arguments but 1 was given ============================== 1 failed in 0.05s =============================================
  10. 29 29 ©MIXI TDDの実例|リファクタ出来そうな箇所を発⾒   引数で⽜(cow)を渡した時に、⽜の鳴き声 ʻmow!’を返す def bark(name: str

    = 'dog') -> str: if name == 'cat': return 'meow!' elif name == 'cow': return 'mow!' return 'bowwow!' 動物が増えたらifが連なって大変 もっと見通しがよく出来そう?
  11. 31 31 ©MIXI TDDの実例|鳴き声の持ち⽅をリファクタする 動物の名前と鳴き声を if で管理するのは微妙なので、辞書で管理するようにする テストがあるので、安全にリファクタできる! ANIMAL_SOUNDS =

    { 'dog': 'bowwow!', 'cat': 'meow!', 'cow': 'mow!', } def bark(name: str = 'dog') -> str: return ANIMAL_SOUNDS[name] 知らない動物が来た時の対応も必要で は?
  12. 34 34 ©MIXI TDDの実例|テストリストが完了し、⽬的を達成 ANIMAL_SOUNDS = { 'dog': 'bowwow!', 'cat':

    'meow!', 'cow': 'mow!', } class ArgumentError(Exception): pass def bark(names: str = ['dog']) -> str: for name in names: if name not in ANIMAL_SOUNDS: raise ArgumentError('Invalid animal name') return ''.join([ANIMAL_SOUNDS[name] for name in names]) import pytest from app.bark import bark, ArgumentError def test_bark(): assert bark() == 'bowwow!' assert bark(['cat']) == 'meow!' assert bark(['cow']) == 'mow!' assert bark(['cow', 'cat']) == 'mow!meow!' with pytest.raises(ArgumentError) as e: bark('pig') assert str(e.value) == 'Invalid animal name' 本実装 テスト
  13. 36 36 ©MIXI TDDの注意点 • 最初はリストのうちの1つを満たすテストからはじめる ◦ 全部のテストを書くと、通すのにも、再設計にも時間がかかる。 • テストを通すとき(Greenステップ)にリファクタリングはしない

    ◦ 設計に関する判断はリファクタリングのステップで。⽬の前に集中。 • ⼀度に必要以上のリファクタリングをしない ◦ フローを回す速度が遅くなってしまう。
  14. 39 39 ©MIXI テストが書きづらい... def get_active_user_x_acounts (): from_time = datetime.now()

    - timedelta(days=3) # アクティブなユーザーを取ってくる処理 users = query(User).filter(user.updated_at > from_time).all() active_users = [] for user in users: # user が active かどうかのスコアを得る複雑な処理 active_score = # ... # 設定値を元にアクティブユーザーかどうかを判定 if active_score > Config.ACTIVE_USER_THRESHOLD: active_users.append(user) # ユーザーの X アカウント情報を取ってくる処理 result = [] for user in active_users: # 外部サイトへのリクエストをして X アカウント情報を取得 user_x_account = request( 'GET', 'https://x.com/users/~~~' ) result.append(user_x_account) return result
  15. 40 40 ©MIXI テストが書きづらい... def get_active_user_x_acounts (): from_time = datetime.now()

    - timedelta(days=3) # アクティブなユーザーを取ってくる処理 users = query(User).filter(user.updated_at > from_time).all() active_users = [] for user in users: # user が active かどうかのスコアを得る複雑な処理 active_score = # ... # 設定値を元にアクティブユーザーかどうかを判定 if active_score > Config.ACTIVE_USER_THRESHOLD: active_users.append(user) # ユーザーの X アカウント情報を取ってくる処理 result = [] for user in active_users: # 外部サイトへのリクエストをして X アカウント情報を取得 user_x_account = request( 'GET', 'https://x.com/users/~~~' ) result.append(user_x_account) return result 現在時刻、DBのレコードに依存 複雑なロジック 関数外の設定で挙動が変わる 外部リクエストに依存 テストパターン多くて重い、見づらい
  16. 41 41 ©MIXI テストを書きやすくするには?|テスタビリティを構成する特性 • 実⾏円滑性(Operability) ◦ テスト実⾏の際の⽀障のなさ。実⾏速度、バグの総数など。 • 観測容易性(Observability)

    ◦ テスト対象の出⼒、エラー、内部状態を確認しやすいか? • 制御容易性(Controllability) ◦ テスト対象はテストを⾏うための操作がしやすいか?(テスト⽤に内部の挙動を操作できるか?) • 分解容易性(Decomposability) ◦ 実⾏するテストの範囲を分離しやすいか?(依存をモックしたりして対象範囲のみテスト可能か?) • 単純性(Simplicity) ◦ テスト対象のコードや仕様はシンプルか? • 安定性(Stability) ◦ テスト対象はテスト負担を増やすような変更が少ないか?(仕様変更やそれによる影響が少ないか) • 理解容易性(Understandability) ◦ テストを⾏うための情報を得やすいか?(仕様書があるかなど)
  17. 42 42 ©MIXI テスタビリティの特性の観点で⾒てみる def get_active_user_x_acounts (): from_time = datetime.now()

    - timedelta(days=3) # アクティブなユーザーを取ってくる処理 users = query(User).filter(user.updated_at > from_time).all() active_users = [] for user in users: # user が active かどうかのスコアを得る複雑な処理 active_score = # ... # 設定値を元にアクティブユーザーかどうかを判定 if active_score > Config.ACTIVE_USER_THRESHOLD: active_users.append(user) # ユーザーの X アカウント情報を取ってくる処理 result = [] for user in active_users: # 外部サイトへのリクエストをして X アカウント情報を取得 user_x_account = request( 'GET', 'https://x.com/users/~~~' ) result.append(user_x_account) return result 現在時刻、DBのレコードに依存 → 内部挙動を制御しづらい (制御容易性が低い)
  18. 43 43 ©MIXI テスタビリティの特性の観点で⾒てみる def get_active_user_x_acounts (): from_time = datetime.now()

    - timedelta(days=3) # アクティブなユーザーを取ってくる処理 users = query(User).filter(user.updated_at > from_time).all() active_users = [] for user in users: # user が active かどうかのスコアを得る複雑な処理 active_score = # ... # 設定値を元にアクティブユーザーかどうかを判定 if active_score > Config.ACTIVE_USER_THRESHOLD: active_users.append(user) # ユーザーの X アカウント情報を取ってくる処理 result = [] for user in active_users: # 外部サイトへのリクエストをして X アカウント情報を取得 user_x_account = request( 'GET', 'https://x.com/users/~~~' ) result.append(user_x_account) return result 複雑なロジック → 仕様が難しい&変更があるとテストも 大幅に変わる... 処理ベタ書きでモックもできない... (分解容易性、単純性、安定性が低い)
  19. 44 44 ©MIXI テスタビリティが上がるようにリファクタしてみる def get_active_user_x_acounts (from_time): # アクティブなユーザーを取ってくる処理 users

    = get_users_recently_updated(from_time) active_users = select_active_users(users, Config.ACTIVE_USER_THRESHOLD) result = [] for user in active_users: user_x_account = get_user_x_acounts(user) result.append(user_x_account) return result def get_users_recently_updated (from_time): return query(User).filter(user.updated_at > from_time).all() def select_active_users (users, active_score_threshold ): return list(filter(None, [user for user in users if calc_active_score(user) > active_score_threshold else None])) def calc_active_score (user): # user が active かどうかのスコアを得る複雑な処理 return active_score def get_user_x_account (user): # 外部サイトへのリクエストをして X アカウント情報を取得 return request('GET', f'https://x.com/users/~~~' )
  20. 45 45 ©MIXI テスタビリティが上がるようにリファクタしてみる def get_active_user_x_acounts (from_time): # アクティブなユーザーを取ってくる処理 users

    = get_users_recently_updated(from_time) active_users = select_active_users(users, Config.ACTIVE_USER_THRESHOLD) result = [] for user in active_users: user_x_account = get_user_x_acounts(user) result.append(user_x_account) return result def get_users_recently_updated (from_time): return query(User).filter(user.updated_at > from_time).all() def select_active_users (users, active_score_threshold ): return list(filter(None, [user for user in users if calc_active_score(user) > active_score_threshold else None])) def calc_active_score (user): # user が active かどうかのスコアを得る複雑な処理 return active_score def get_user_x_account (user): # 外部サイトへのリクエストをして X アカウント情報を取得 return request('GET', f'https://x.com/users/~~~' ) 現在時刻を外部から指定できる! = 制御容易性 DBから取得する処理をモックしやすい! = 分解容易性
  21. 46 46 ©MIXI テスタビリティが上がるようにリファクタしてみる def get_active_user_x_acounts (from_time): # アクティブなユーザーを取ってくる処理 users

    = get_users_recently_updated(from_time) active_users = select_active_users(users, Config.ACTIVE_USER_THRESHOLD) result = [] for user in active_users: user_x_account = get_user_x_acounts(user) result.append(user_x_account) return result def get_users_recently_updated (from_time): return query(User).filter(user.updated_at > from_time).all() def select_active_users (users, active_score_threshold ): return list(filter(None, [user for user in users if calc_active_score(user) > active_score_threshold else None])) def calc_active_score (user): # user が active かどうかのスコアを得る複雑な処理 return active_score def get_user_x_account (user): # 外部サイトへのリクエストをして X アカウント情報を取得 return request('GET', f'https://x.com/users/~~~' ) 仕様が複雑な部分を個別テストできる! 本体のメソッドへの影響が減った! = 分解容易性, 単純性, 安定性
  22. 50 50 ©MIXI テスト技法 • テストのレベル ◦ 単体テスト (Unit testing)

    ▪ もっとも⼩さなテスト ▪ クラス、メソッド単位 (⾔語‧テストライブラリ等で異なる) ◦ 統合テスト (Integration testing) ▪ 単体テストよりも⼤きな範囲のテスト ◦ システムテスト (System testing) ▪ ソフトウェア全体のテスト ◦ 受け⼊れテスト (User Acceptance Testing) ▪ 顧客がソフトウェアを受け⼊れる時のテスト
  23. 51 51 ©MIXI テスト技法 • テストの種別 ◦ ブラックボックステスト ▪ 仕様や要件に基づいてテストを実施するテスト

    ▪ 実装レベルの知識は必要としない ◦ ホワイトボックステスト ▪ 実装レベルの知識に基づいて実施するテスト ▪ ソフトウェアの内部パス、構造、実装 ... ◦ グレーボックステスト ▪ 実装をある程度調べた上で、ブラックボックステストのテストケースを効 率的に選択していく
  24. 52 52 ©MIXI テスト技法 • ブラックボックステスト ◦ 同値クラステスト ◦ 境界値テスト

    ◦ デシジョンテーブルテスト ◦ ペア構成テスト ◦ …. • ホワイトボックステスト ◦ 制御フローテスト ◦ データフローテスト
  25. 56 56 ©MIXI その他テストに関するTIPS • テストを書くのは⼤事だが、なるべく数を減らしましょう ◦ 特に重いテストが多いと実⾏時間が伸びて⽣産性が下がる • 確率的に落ちるテスト(Flaky

    Test)に対処していく ◦ ⾃動テストの再実⾏が何度も必要だと、⽣産性は下がるし、CIのコストも上がる ◦ テスト成功率などを監視して、よく落ちるものは早めに対処する • QAエンジニアの⽅とはなるべく仕様設計の段階からコミュニケーションを取る ◦ 早い段階で仕様の擦り合わせをすることで、擦り合わせのコストを減らしつつ、異常ケー スやQA実⾏の効率化について早めに対処ができる • 重要なのは成果‧⽣産性が上がること ◦ ⾃分が⼀⼈で頑張る以上に⽂化を作るのを頑張る ◦ ⽅法論は重要だが、それに縛られず常に最善を⽬指すのが重要と考えています
  26. 58 58 ©MIXI ペアプログラミング • ドライバー ◦ 実際に操作する⼈ • ナビゲーター

    ◦ ドライバーの操作を眺めつつ、助ける⼈ • 定期的に役割を⼊れ替えながら進める
  27. 59 59 ©MIXI ペアプログラミング うまくやるコツ • ドライバー ◦ 今、何をやろうとしているか、やっているかを明確にする (発⾔する)

    • ナビゲーター ◦ 良い⽅法を思いついたり、ミスに気づいたりしたときに、積極的に発⾔する ◦ ドライバーが何をやろうとしていることが良くわからなくなったら、すぐに聞く commit & push してれば、役割交代はしやすいはず‧‧‧?
  28. 62 62 ©MIXI コードレビューをしよう • チームごとにteam-A,B,C ブランチ向きにPRを作ってください • レビューするチーム •

    演習1: A → B、 B → C、 C → A • 演習2: A → C、 B → A、 C → B • 時間が余ったら他のチームをレビューしてもOK
  29. 64 64 ©MIXI コードレビューをしよう • どうすれば、レビューが通りやすいかを考えよう • PR の説明をしっかり書こう ◦

    どういう背景、理由で、どういうものを作った、など ◦ 重点的にレビューして欲しいところや、実装していてよく分からなかったところ、など ◦ ⾏単位でコメント/会話ができるのでそれも活⽤しよう • JIRA のチケットや、関連 PR, issue など ◦ 背景の詳細や、仕様などを追いやすい ◦ 監査などのときに、追いやすい、など • どういうタイミングでマージして欲しい、など (QA終わるまで待って、など)
  30. 65 65 ©MIXI コードレビューをしよう • レビューは⼈格攻撃ではない (⼼理的安全性) ◦ レビューする側もされる側も攻撃ではないことを意識する •

    わからないところは聞こう • 褒めよう! • この⼈はこういうところをレビューしてくるだろうなぁと考えてみる
  31. 66 66 ©MIXI 参考⽂献 • SQuBOK Guide V3 • テスト駆動開発

    • 【翻訳】テスト駆動開発の定義 • 保守しやすく変化に強いソフトウェアを⽀える柱 ⾃動テストとテスト駆動開 発 、その全体像 • はじめて学ぶソフトウェアのテスト技法 • ソフトウェアテスト技法 • 「テスタビリティ」への投資で⽣産性向上!評価の指標と向上させるポイント