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
JJUG_CCC_2024 プロダクトが変われば、テストも変わる
Search
mashi
October 27, 2024
7
1.1k
JJUG_CCC_2024 プロダクトが変われば、テストも変わる
mashi
October 27, 2024
Tweet
Share
More Decks by mashi
See All by mashi
Nstockの認知負荷へのアプローチ@CHUO_TECH_20240619
yukishima
0
86
Featured
See All Featured
Building a Scalable Design System with Sketch
lauravandoore
460
33k
[RailsConf 2023] Rails as a piece of cake
palkan
53
5.1k
GraphQLの誤解/rethinking-graphql
sonatard
68
10k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
280
13k
Practical Orchestrator
shlominoach
186
10k
Navigating Team Friction
lara
183
15k
Bash Introduction
62gerente
610
210k
VelocityConf: Rendering Performance Case Studies
addyosmani
327
24k
Statistics for Hackers
jakevdp
797
220k
The Cost Of JavaScript in 2023
addyosmani
46
7.2k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
172
50k
Code Reviewing Like a Champion
maltzj
521
39k
Transcript
プロダクトが変われば、テストも変わる JJUG CCC 2024 Fall Nstock株式会社 Yuki SHIMA
Yuki SHIMA Nstock 株式会社のソフトウェアエンジニア 最近上野に引っ越しました。富山県出身。 Javaを本格的に書いたのはNstockに入って から、今1年目。 登壇初めてです。緊張してます。早く酒を飲 みたい。
はじめに
株式報酬SaaSの場合
- お客様は数社 - それぞれの機能群が独立して存在する - メンバーそれぞれが機能をバックエンドからフロントエンドまで実装するスタイル - 基本的にはデータのCRUDで機能が表現されている ローンチから間もないとき
次第に状況も変わり得る - 導入社数は数十倍 - 機能同士の関連が出てきた。料金プランによる制御などもある - 大きな機能をチームで一緒に開発するスタイル - シンプルなCRUDでは表現しきれず、データの整合性の単位は複数のテーブルをま たぐことが普通
機能の増大にともない相互依存性や分岐の複雑さも増大し、デグレなども発生
リリースしたものはだいたい間違っている 自分たちの例 - ストックオプションを取り扱うSaaSは日本には少なく、そもそもどのような機能 がお客様に刺さるかがわからない - 自分たちがいいと思っているUI/UXがお客様にとってもそうかはわからない - 開発したときに思い描いたよりもストックオプションは多種多様で、お客様ごとの オペレーションも多種多様
- やりたいこと、やるべきことは初期の想定を大抵の場合上回る
つまり、間違っている前提で早く変えていく必要がある
- 次の2つの項目に「同意できる」と回答できる組織であ ればハイパフォーマーである可能性が高い - テストの大半を、統合環境を必要とせず実施できる - アプリケーションを独立した形式で、デプロイまた はリリースできる 「LeanとDevOpsの科学」より テスト容易性とデプロイ容易性
自分たちは デプロイ容易性は比較的高そう(CI/CDは自動化されて いる。まだアプリケーションも小さく、インフラもシン プル)
テスト容易性・・・?うっ・・・
1. テスト実行の遅さ: ◦ 全レイヤを通過するテストが多いため、1回のテスト実行に長時間を要する。 2. バグの検出難度: ◦ インテグレーションテストでは小さな変更による影響を特定するのが困難。 3. メンテナンスの複雑さ:
◦ テストデータ準備が複雑で、システムの変更に応じた更新が大変。 当時の自動テストの状況 やり方:DockerでPostgreSQL・Spring bootを起動しWeb APIのリクエスト・レスポ ンス・データベースの状態をまるっとテスト
すべてインテグレーションテストで行っていた - 正しい動作 - リソースが見つからない - ユーザが入力したSO個数のバリデーション(マイナスを入れられない) - 付与されているSO個数からユーザが入力したSO個数を引くときのバリデーション こんなイメージ
すべてのケースが実データベースを通しているので遅い 各種テストケースごとのテストデータを用意するのがめっちゃ大変(CSVで管理してい るので静的解析がきかせにくく、テーブルスキーマが変わったときにつらい
- 優れたソフトウェアを開発するには、プログラマはフィード バックを必要とします。できる限り頻繁かつ素早いフィード バックが必要です。優れたテスト戦略は、フィードバック ループを短くするので、効率的に仕事ができます。 「ベタープログラマ」より できるだけ早くフィードバックを得たい
💡テストは書いているが、もっと効果的にできそう
やったこと - 輪読会の実施 - テストのための共通言語を作る - 単体テストの割合を増やす - レイヤーに合わせてI/Oを伴わずにロジックを切り出す -
パラメタライズテストの導入 - ライブラリやLLMの利用 - テストのためのパラメータ
その1:輪読会の実施 - 読まなくてもいい輪読会を実施 - 詳しくはテックブログで - 「単体テストの考え方/使い方」を読んだ - 自分たちどのようにテストをしているか -
どんなテストをしていくといいかを議論 👉テストにおける共通言語を作った
その2:単体テストの割合を増やす
の前に、単体テストとは??
- 単体テストというキーワードは人によってもゆらぎがあ る - 今自分たちにとって必要なのは「どんなテストをどう やって書くのか」の指針であり、言葉が揺らぐともった いない テストをサイズでわける https://x.com/t_wada/status/1 184313487876476928
当時はSmallなテストが極めて少なく(1%以下)ほとんど がMedium以上
プラスして、テストの検証方法による分類 1. 出力値ベーステスト ◦ 特定の入力に対する関数やメソッドの出力(戻り値)を検証 ◦ 例:メソッドの戻り値を検証する 2. 状態ベーステスト ◦
操作後のシステムやオブジェクトの状態を検証 ◦ 例:テーブルに新しいレコードが追加されたことを確認する 3. コミュニケーションベーステスト ◦ テストダブルを使用して、クラス間のメソッド呼び出しを検証 ◦ 例:サービスクラスがリポジトリのsaveメソッドを正しいパラメータで呼び 出していることを確認
具体的な自動テストとコード
例:申請されたストックオプションの個数を印字した帳 票をPDFで出力したい • 印字される内容は従業員の氏名、住所、ストックオプションの個数 • 印字に必要なデータはPostgreSQLに保存されている • 帳票には複数の申請が紐づけられる • 申請に含まれるストックオプションの回合は最大4種類まで
• 帳票PDFは証券会社によってフォーマットが異なる • 帳票PDFはクラウドストレージ(S3)に保存される
ざっくりこんな構成
エンティティのテスト
エンティティのテスト ビルダーパターンで テストパラメータを作成 イミュータブルにして 出力値ベースで検証 I/Oを伴わない 🍎はやい
- 従業員、住所、ストックオプションなどのエンティティが正しく構造化され、デー タが期待通りに保持されているか。 - データのバリデーション(例:ストックオプションの個数が負でない、回合が4種 類以内)が正しく機能しているか。 エンティティテストの役割 文字数制限、算出ロジックやステータスの変更、日付判定などが正しく行われること 検証方法:(副作用を廃しイミュータブルに)戻り値を検証する出力値ベーステスト
ユースケースのテスト:正常動作
ユースケースのテスト:正常動作 正しくメソッドが 呼ばれたことを検証 依存するオブジェクトは モック それぞれのオブジェクト(PDF生成やリポジトリ) が正しい引数で正しい回数呼ばれたかを検証する
ユースケースのテスト:異常動作
ユースケースのテスト:異常動作 意図通りの例外が throwされていることを検証 正しく「呼ばれない」ことを検証
それぞれのクラスが正しく呼ばれているか、実行順が正しいかをコミュニケーション ベーステスト(テストダブルを使用して、クラス間のメソッド呼び出しを検証) ユースケーステストの役割 - 各証券会社向けに適切な帳票フォーマットが選択され、生成されるか。 - データ取得から帳票生成までのフローが期待通りに動作するか。 - 意図しない状態が発生したとき正しい例外を投げられるか。
PDF生成のテスト
PDF生成のテスト あらかじめ用意したPDFと 比較する PDFGeneratorは引数だけで 動く(DBアクセスなどは行 わずに外から引き渡す)
特定のパラメータのときに正しく印字されることを、PDFの出力値ベーステスト 帳票生成ロジックテストの役割 - 従業員の氏名、住所、ストックオプションの個数が正しく印字されるか。 - 複数の申請に対応して帳票が正しく出力されるか。 - 4種類までのストックオプションの回合が正しく帳票に反映されるか。
プレゼンテーションのテスト
プレゼンテーションのテスト 実際のデータベースの値も 検証する JSONのレスポンスを検証 テスト用のデータベースを実際に起動してテスト
Web APIとして正しく動作するように、テーブルの状態ベーステストとレスポンスの 出力値ベーステストを併用 プレゼンテーションテストの役割 - 最も長い正常系(ロングレンジハッピーパス)で動くことを期待 - 例えば帳票に印字されるSO個数が1個〜10000個、従業員10名分など - RDB、LocalStackなどを利用し、できるだけMockしない
None
やってみて - もともとは9割がプレゼンテーション層のインテグレーションテスト - 今はエンティティ層の単体テストが半分以上 - エンティティのテストは極めて速いのでローカルで何度も試すことが苦にならない - 今まではデータベースまで疎通してテストをしていたので遅かった -
できるだけクラスに対して「テストする内容を減らそう」というアプローチになる ので自然に単一責任原則が守られるようになっていく
自動テストを通じてプロダクションコードが 改善されていく👏👏👏
🤖その3:ライブラリやLLMの利用
- Javaのビルダーパターンを自動生成するアノテーションプロセッサ - 既存クラスの変更なしにビルダークラスを生成 - Lombokなどとも統合可能(自分たちは使っていない) - Staged Builderパターンをサポートしており、タイプセーフにオブジェクトを構築 -
これから使っていきたい! 特にテストデータで一部のパラメータを変更したいなどのケースでめっちゃ使える おすすめ1:Jilt(https://github.com/skinny85/jilt)
TODO パラメタライズドテストと合わせたコード おすすめ1:Jilt(https:/ /github.com/skinny85/jilt) テストに限定するためアノテーションを付与 (実際にはRecordクラスのためにプライベートメソッ ドでも利用) 1. テストで使いたいデフォルト値を設定 2.
copyメソッドで差し替える
- Cursorを使うとかなり楽 - ざっくりこんな手順 1. 手で理想形のテストを書く(テストメソッドやexpectもできるだけ丁寧に 2. 1で書いたテストファイルを開いてテストを書くとサジェストされる a. もしこれでだめならチャットから「1みたいな感じでテストを書いて」
おすすめ2:LLMの利用
おすすめ2:LLMの利用 - LLMは自分たちのコード(や事業)を学習しているわけではない - 境界値や異常値に関しては抜けやすい - 独特なビジネスロジックは任せづらい 所感)めちゃくちゃ楽な一方で、偽陰性(プロダクションコードは間違っているが、テ ストが成功するテストは出力されやすい)傾向にある
おまけ:コード自体を変えていく - どこからでも呼ばれていたSetterを廃し、カプセル化する - ファクトリメソッドを用いてコンテキストにあった初期化を行う - 複数個で意味があるものは、ファーストクラスコレクション - Spring Data
JDBCの集約機能を使い、データの整合性を取る - Record Classを積極的に利用、ボイラープレートをなくしイミュータブルに - ⭐ここでもjiltが輝く! - 手続き的なif文をやめ、switch式で全域関数にしていく
✅ここ一年でどう変わったか
1. テスト実行の遅さ: ◦ 全レイヤを通過するテストが多いため、1回のテスト実行に長時間を要する。 2. バグの検出難度: ◦ インテグレーションテストでは小さな変更による影響を特定するのが困難。 3. メンテナンスの複雑さ:
◦ テストデータ準備が複雑で、システムの変更に応じた更新が大変。 当時の自動テストの状況 やり方:DockerでPostgreSQL・Spring bootを起動しWeb APIのリクエスト・レスポ ンス・データベースの状態をまるっとテスト
1. テスト実行の遅さ ◦ 単体テストが増えたことでローカルで高速にイテレーションを回せるように 2. バグの検出難度 ◦ パラメタライズテストで網羅率UP 3. メンテナンスの複雑さ
◦ 各レイヤごとにテストを書けるようになった ◦ ビルダーパターンの導入、LLMの利用でデータ準備のコストも圧縮 今の自動テストの状況 方針:レイヤごとにテスト方針を定め、テストサイズとテストスコープを小さくする
数字を見てみる(2024-01-01と2024-10-24の比較) デプロイ頻度やリードタイム、MTTRも概ねキープか向上している
これからの自動テストのやっていき 最近の実装ではC0 95%, C1 100%! (今はカバレッジは目標にしてない) 統合的なシナリオをE2Eでテストしたい (今は検証段階)
主張
プロダクトがシンプルなうちはテストもシンプルでいい 1. スタートアップやPMF前などはドメインの理解も浅く、適切な設計も難しい ◦ テストで大外を守れていれば、積極的にリファクタリングできる 2. 大体の要件はあとからやってくる ◦ 最初から完璧な設計を目指すのではなく、変更容易性が重要 3.
No 直感的な実装、Yes テストのし易い実装 ◦ 直感的な実装はひとによってかなり差がある。テストがしやすい実装は比較 的共通の理解を作りやすい。
早くフィードバックを得られることが自動テストのよさ 1. DBアクセスを伴わないテストならローカルで一瞬でわかる ◦ ガシガシ書いてガシガシ直す ◦ 手を動かしたほうが理解が早くできることもある 2. 遅いテストはそれだけで設計を歪ませる ◦
テストの待ち時間が長いことで、後回しにされる「ちょっとした改善」 ◦ 高速なテストにすることで「すぐ直す」をくせにできる ◦ (これあるあるだと思います)
まとめ
プロダクトが変われば、テストも変わる プロダクトや事業のフェーズ、システムのサイズに応じて適切なテストがある あのときのベストが今もベストとは限らない。それは設計もテストも同じ。 早く小さく試す、失敗するための最小単位は「自動テスト」
ご清聴ありがとうございました