Slide 1

Slide 1 text

プロダクトが変われば、テストも変わる JJUG CCC 2024 Fall Nstock株式会社 Yuki SHIMA

Slide 2

Slide 2 text

Yuki SHIMA Nstock 株式会社のソフトウェアエンジニア 最近上野に引っ越しました。富山県出身。 Javaを本格的に書いたのはNstockに入って から、今1年目。 登壇初めてです。緊張してます。早く酒を飲 みたい。

Slide 3

Slide 3 text

はじめに

Slide 4

Slide 4 text

株式報酬SaaSの場合

Slide 5

Slide 5 text

- お客様は数社 - それぞれの機能群が独立して存在する - メンバーそれぞれが機能をバックエンドからフロントエンドまで実装するスタイル - 基本的にはデータのCRUDで機能が表現されている ローンチから間もないとき

Slide 6

Slide 6 text

次第に状況も変わり得る - 導入社数は数十倍 - 機能同士の関連が出てきた。料金プランによる制御などもある - 大きな機能をチームで一緒に開発するスタイル - シンプルなCRUDでは表現しきれず、データの整合性の単位は複数のテーブルをま たぐことが普通 機能の増大にともない相互依存性や分岐の複雑さも増大し、デグレなども発生

Slide 7

Slide 7 text

リリースしたものはだいたい間違っている 自分たちの例 - ストックオプションを取り扱うSaaSは日本には少なく、そもそもどのような機能 がお客様に刺さるかがわからない - 自分たちがいいと思っているUI/UXがお客様にとってもそうかはわからない - 開発したときに思い描いたよりもストックオプションは多種多様で、お客様ごとの オペレーションも多種多様 - やりたいこと、やるべきことは初期の想定を大抵の場合上回る

Slide 8

Slide 8 text

つまり、間違っている前提で早く変えていく必要がある

Slide 9

Slide 9 text

- 次の2つの項目に「同意できる」と回答できる組織であ ればハイパフォーマーである可能性が高い - テストの大半を、統合環境を必要とせず実施できる - アプリケーションを独立した形式で、デプロイまた はリリースできる 「LeanとDevOpsの科学」より テスト容易性とデプロイ容易性

Slide 10

Slide 10 text

自分たちは デプロイ容易性は比較的高そう(CI/CDは自動化されて いる。まだアプリケーションも小さく、インフラもシン プル)

Slide 11

Slide 11 text

テスト容易性・・・?うっ・・・

Slide 12

Slide 12 text

1. テスト実行の遅さ: ○ 全レイヤを通過するテストが多いため、1回のテスト実行に長時間を要する。 2. バグの検出難度: ○ インテグレーションテストでは小さな変更による影響を特定するのが困難。 3. メンテナンスの複雑さ: ○ テストデータ準備が複雑で、システムの変更に応じた更新が大変。 当時の自動テストの状況 やり方:DockerでPostgreSQL・Spring bootを起動しWeb APIのリクエスト・レスポ ンス・データベースの状態をまるっとテスト

Slide 13

Slide 13 text

すべてインテグレーションテストで行っていた - 正しい動作 - リソースが見つからない - ユーザが入力したSO個数のバリデーション(マイナスを入れられない) - 付与されているSO個数からユーザが入力したSO個数を引くときのバリデーション こんなイメージ すべてのケースが実データベースを通しているので遅い 各種テストケースごとのテストデータを用意するのがめっちゃ大変(CSVで管理してい るので静的解析がきかせにくく、テーブルスキーマが変わったときにつらい

Slide 14

Slide 14 text

- 優れたソフトウェアを開発するには、プログラマはフィード バックを必要とします。できる限り頻繁かつ素早いフィード バックが必要です。優れたテスト戦略は、フィードバック ループを短くするので、効率的に仕事ができます。 「ベタープログラマ」より できるだけ早くフィードバックを得たい

Slide 15

Slide 15 text

💡テストは書いているが、もっと効果的にできそう

Slide 16

Slide 16 text

やったこと - 輪読会の実施 - テストのための共通言語を作る - 単体テストの割合を増やす - レイヤーに合わせてI/Oを伴わずにロジックを切り出す - パラメタライズテストの導入 - ライブラリやLLMの利用 - テストのためのパラメータ

Slide 17

Slide 17 text

その1:輪読会の実施 - 読まなくてもいい輪読会を実施 - 詳しくはテックブログで - 「単体テストの考え方/使い方」を読んだ - 自分たちどのようにテストをしているか - どんなテストをしていくといいかを議論 👉テストにおける共通言語を作った

Slide 18

Slide 18 text

その2:単体テストの割合を増やす

Slide 19

Slide 19 text

の前に、単体テストとは??

Slide 20

Slide 20 text

- 単体テストというキーワードは人によってもゆらぎがあ る - 今自分たちにとって必要なのは「どんなテストをどう やって書くのか」の指針であり、言葉が揺らぐともった いない テストをサイズでわける https://x.com/t_wada/status/1 184313487876476928 当時はSmallなテストが極めて少なく(1%以下)ほとんど がMedium以上

Slide 21

Slide 21 text

プラスして、テストの検証方法による分類 1. 出力値ベーステスト ○ 特定の入力に対する関数やメソッドの出力(戻り値)を検証 ○ 例:メソッドの戻り値を検証する 2. 状態ベーステスト ○ 操作後のシステムやオブジェクトの状態を検証 ○ 例:テーブルに新しいレコードが追加されたことを確認する 3. コミュニケーションベーステスト ○ テストダブルを使用して、クラス間のメソッド呼び出しを検証 ○ 例:サービスクラスがリポジトリのsaveメソッドを正しいパラメータで呼び 出していることを確認

Slide 22

Slide 22 text

具体的な自動テストとコード

Slide 23

Slide 23 text

例:申請されたストックオプションの個数を印字した帳 票をPDFで出力したい ● 印字される内容は従業員の氏名、住所、ストックオプションの個数 ● 印字に必要なデータはPostgreSQLに保存されている ● 帳票には複数の申請が紐づけられる ● 申請に含まれるストックオプションの回合は最大4種類まで ● 帳票PDFは証券会社によってフォーマットが異なる ● 帳票PDFはクラウドストレージ(S3)に保存される

Slide 24

Slide 24 text

ざっくりこんな構成

Slide 25

Slide 25 text

エンティティのテスト

Slide 26

Slide 26 text

エンティティのテスト ビルダーパターンで テストパラメータを作成 イミュータブルにして 出力値ベースで検証 I/Oを伴わない 🍎はやい

Slide 27

Slide 27 text

- 従業員、住所、ストックオプションなどのエンティティが正しく構造化され、デー タが期待通りに保持されているか。 - データのバリデーション(例:ストックオプションの個数が負でない、回合が4種 類以内)が正しく機能しているか。 エンティティテストの役割 文字数制限、算出ロジックやステータスの変更、日付判定などが正しく行われること 検証方法:(副作用を廃しイミュータブルに)戻り値を検証する出力値ベーステスト

Slide 28

Slide 28 text

ユースケースのテスト:正常動作

Slide 29

Slide 29 text

ユースケースのテスト:正常動作 正しくメソッドが 呼ばれたことを検証 依存するオブジェクトは モック それぞれのオブジェクト(PDF生成やリポジトリ) が正しい引数で正しい回数呼ばれたかを検証する

Slide 30

Slide 30 text

ユースケースのテスト:異常動作

Slide 31

Slide 31 text

ユースケースのテスト:異常動作 意図通りの例外が throwされていることを検証 正しく「呼ばれない」ことを検証

Slide 32

Slide 32 text

それぞれのクラスが正しく呼ばれているか、実行順が正しいかをコミュニケーション ベーステスト(テストダブルを使用して、クラス間のメソッド呼び出しを検証) ユースケーステストの役割 - 各証券会社向けに適切な帳票フォーマットが選択され、生成されるか。 - データ取得から帳票生成までのフローが期待通りに動作するか。 - 意図しない状態が発生したとき正しい例外を投げられるか。

Slide 33

Slide 33 text

PDF生成のテスト

Slide 34

Slide 34 text

PDF生成のテスト あらかじめ用意したPDFと 比較する PDFGeneratorは引数だけで 動く(DBアクセスなどは行 わずに外から引き渡す)

Slide 35

Slide 35 text

特定のパラメータのときに正しく印字されることを、PDFの出力値ベーステスト 帳票生成ロジックテストの役割 - 従業員の氏名、住所、ストックオプションの個数が正しく印字されるか。 - 複数の申請に対応して帳票が正しく出力されるか。 - 4種類までのストックオプションの回合が正しく帳票に反映されるか。

Slide 36

Slide 36 text

プレゼンテーションのテスト

Slide 37

Slide 37 text

プレゼンテーションのテスト 実際のデータベースの値も 検証する JSONのレスポンスを検証 テスト用のデータベースを実際に起動してテスト

Slide 38

Slide 38 text

Web APIとして正しく動作するように、テーブルの状態ベーステストとレスポンスの 出力値ベーステストを併用 プレゼンテーションテストの役割 - 最も長い正常系(ロングレンジハッピーパス)で動くことを期待 - 例えば帳票に印字されるSO個数が1個〜10000個、従業員10名分など - RDB、LocalStackなどを利用し、できるだけMockしない

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

やってみて - もともとは9割がプレゼンテーション層のインテグレーションテスト - 今はエンティティ層の単体テストが半分以上 - エンティティのテストは極めて速いのでローカルで何度も試すことが苦にならない - 今まではデータベースまで疎通してテストをしていたので遅かった - できるだけクラスに対して「テストする内容を減らそう」というアプローチになる ので自然に単一責任原則が守られるようになっていく

Slide 41

Slide 41 text

自動テストを通じてプロダクションコードが 改善されていく👏👏👏

Slide 42

Slide 42 text

🤖その3:ライブラリやLLMの利用

Slide 43

Slide 43 text

- Javaのビルダーパターンを自動生成するアノテーションプロセッサ - 既存クラスの変更なしにビルダークラスを生成 - Lombokなどとも統合可能(自分たちは使っていない) - Staged Builderパターンをサポートしており、タイプセーフにオブジェクトを構築 - これから使っていきたい! 特にテストデータで一部のパラメータを変更したいなどのケースでめっちゃ使える おすすめ1:Jilt(https://github.com/skinny85/jilt)

Slide 44

Slide 44 text

TODO パラメタライズドテストと合わせたコード おすすめ1:Jilt(https:/ /github.com/skinny85/jilt) テストに限定するためアノテーションを付与 (実際にはRecordクラスのためにプライベートメソッ ドでも利用) 1. テストで使いたいデフォルト値を設定 2. copyメソッドで差し替える

Slide 45

Slide 45 text

- Cursorを使うとかなり楽 - ざっくりこんな手順 1. 手で理想形のテストを書く(テストメソッドやexpectもできるだけ丁寧に 2. 1で書いたテストファイルを開いてテストを書くとサジェストされる a. もしこれでだめならチャットから「1みたいな感じでテストを書いて」 おすすめ2:LLMの利用

Slide 46

Slide 46 text

おすすめ2:LLMの利用 - LLMは自分たちのコード(や事業)を学習しているわけではない - 境界値や異常値に関しては抜けやすい - 独特なビジネスロジックは任せづらい 所感)めちゃくちゃ楽な一方で、偽陰性(プロダクションコードは間違っているが、テ ストが成功するテストは出力されやすい)傾向にある

Slide 47

Slide 47 text

おまけ:コード自体を変えていく - どこからでも呼ばれていたSetterを廃し、カプセル化する - ファクトリメソッドを用いてコンテキストにあった初期化を行う - 複数個で意味があるものは、ファーストクラスコレクション - Spring Data JDBCの集約機能を使い、データの整合性を取る - Record Classを積極的に利用、ボイラープレートをなくしイミュータブルに - ⭐ここでもjiltが輝く! - 手続き的なif文をやめ、switch式で全域関数にしていく

Slide 48

Slide 48 text

✅ここ一年でどう変わったか

Slide 49

Slide 49 text

1. テスト実行の遅さ: ○ 全レイヤを通過するテストが多いため、1回のテスト実行に長時間を要する。 2. バグの検出難度: ○ インテグレーションテストでは小さな変更による影響を特定するのが困難。 3. メンテナンスの複雑さ: ○ テストデータ準備が複雑で、システムの変更に応じた更新が大変。 当時の自動テストの状況 やり方:DockerでPostgreSQL・Spring bootを起動しWeb APIのリクエスト・レスポ ンス・データベースの状態をまるっとテスト

Slide 50

Slide 50 text

1. テスト実行の遅さ ○ 単体テストが増えたことでローカルで高速にイテレーションを回せるように 2. バグの検出難度 ○ パラメタライズテストで網羅率UP 3. メンテナンスの複雑さ ○ 各レイヤごとにテストを書けるようになった ○ ビルダーパターンの導入、LLMの利用でデータ準備のコストも圧縮 今の自動テストの状況 方針:レイヤごとにテスト方針を定め、テストサイズとテストスコープを小さくする

Slide 51

Slide 51 text

数字を見てみる(2024-01-01と2024-10-24の比較) デプロイ頻度やリードタイム、MTTRも概ねキープか向上している

Slide 52

Slide 52 text

これからの自動テストのやっていき 最近の実装ではC0 95%, C1 100%! (今はカバレッジは目標にしてない) 統合的なシナリオをE2Eでテストしたい (今は検証段階)

Slide 53

Slide 53 text

主張

Slide 54

Slide 54 text

プロダクトがシンプルなうちはテストもシンプルでいい 1. スタートアップやPMF前などはドメインの理解も浅く、適切な設計も難しい ○ テストで大外を守れていれば、積極的にリファクタリングできる 2. 大体の要件はあとからやってくる ○ 最初から完璧な設計を目指すのではなく、変更容易性が重要 3. No 直感的な実装、Yes テストのし易い実装 ○ 直感的な実装はひとによってかなり差がある。テストがしやすい実装は比較 的共通の理解を作りやすい。

Slide 55

Slide 55 text

早くフィードバックを得られることが自動テストのよさ 1. DBアクセスを伴わないテストならローカルで一瞬でわかる ○ ガシガシ書いてガシガシ直す ○ 手を動かしたほうが理解が早くできることもある 2. 遅いテストはそれだけで設計を歪ませる ○ テストの待ち時間が長いことで、後回しにされる「ちょっとした改善」 ○ 高速なテストにすることで「すぐ直す」をくせにできる ○ (これあるあるだと思います)

Slide 56

Slide 56 text

まとめ

Slide 57

Slide 57 text

プロダクトが変われば、テストも変わる プロダクトや事業のフェーズ、システムのサイズに応じて適切なテストがある あのときのベストが今もベストとは限らない。それは設計もテストも同じ。 早く小さく試す、失敗するための最小単位は「自動テスト」

Slide 58

Slide 58 text

ご清聴ありがとうございました