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
20年もののレガシーJavaプロダクトに自動テストを組み込んできた話
Search
げんげん
November 14, 2025
0
240
20年もののレガシーJavaプロダクトに自動テストを組み込んできた話
JJUG CCC 2025 Fallの登壇資料です。
X
https://x.com/gengenmusic0719
lit.link
https://lit.link/gengen0719
げんげん
November 14, 2025
Tweet
Share
More Decks by げんげん
See All by げんげん
re:Invent初参加を経て変わったこと
gengen0719
0
180
AmazonQ CLIくんはなんかちょっとかわいい
gengen0719
0
190
AWS_SUMMITでの学びを活かしAI Agentに養ってもらって花を愛でて生きたい
gengen0719
0
130
ずんだもんがお昼ご飯を食べられる時間を教えてくれるLINEbotを作った話
gengen0719
0
210
Featured
See All Featured
How to Think Like a Performance Engineer
csswizardry
28
2.3k
Statistics for Hackers
jakevdp
799
220k
We Have a Design System, Now What?
morganepeng
54
7.9k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Leading Effective Engineering Teams in the AI Era
addyosmani
9
1.1k
The Cult of Friendly URLs
andyhume
79
6.7k
The Power of CSS Pseudo Elements
geoffreycrofte
80
6.1k
Stop Working from a Prison Cell
hatefulcrawdad
272
21k
Git: the NoSQL Database
bkeepers
PRO
432
66k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
285
14k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
127
54k
Transcript
20年もののレガシーJava プロダクトに⾃動テストを 組み込んできた話 2025.11.15
げんげん @gengenmusic0719 奈良県に住むテストと設計が好きなアプリエンジニア お仕事 20年ものの基幹業務システムの保守開発チームのマネージャー →7⽉からマネージャーをやめて新規プロダクトの開発中 趣味 ⾳楽制作, Among Us(最近場が⽴たずかなしいのでどこかで開催されていたら呼んでください)
好きな技術トピック Java、AWS、設計、テスト、CICD、Serverless https://lit.link/gengen0719
保守してきたプロダクトの紹介 とあるERPパッケージソフト 保守期間20年越え Javaソースコード 約380万⾏ 開発組織の⼈員は 約50名 Java 1.4時代のコードが残っている 当初からWebアプリとして作られているので、JSPの実装が残っていたり、
⾊んなフレームワークの実装が地層のように積み重なったりしている。 このプロダクトに⾃動テストの仕組みを整備してきた経験と 新しいプロダクトを作る際に⾃動テストを整備している経験をもとに話します。
想定するオーディエンスとメッセージ - 長く運用しているアプリケーションに - これから自動テストを組み込みたい方 - 組み込もうとしてうまくいかなかった方 - 他社事例が知りたい方
認識を合わせるために 初めに⼀般論の話をします
ソフトウェアの検証を ⾃動化する重要性
ソフトウェアと検証 ソフトウェアはユーザーに価値を届けるためのものです。 価値を届けるためにはまず意図通りの動作ができる必要があります。 そのために検証を⾏います。 また価値を届け続けるためにソフトウェアには変更が加えられ続けます。 - 運⽤をしてみたら不具合が⾒つかった - 運⽤をしてみたら機能追加が必要になった -
依存するソフトウェアの変化の影響を受けて修正が必要になった - 時代の変化で価値が変わり修正が必要になった 変更が加わると検証は再度実施する必要があります。
検証における絶対的な原則 どちらが簡単ですか? - 1⾏の変更を検証したら失敗しました、問題を修正してください - 1万⾏の変更を検証したら失敗しました、問題を修正してください 検証を⾏う単位を⼩さくする⽅がほぼ確実に修正が楽
ソフトウェアの検証を⾃動化する重要性 しかしその検証に⼈間の⼿が必要になると、費⽤が発⽣し時間もかかる。 費⽤が発⽣し時間がかかると、検証を⾏う頻度が下がったり、やらなくなったりする。 そうなると気づけばたくさんの修正が⼊っていて、どこで問題が起きているのかわからなくなる。 検証の頻度を落とすことは負のループを⽣む。 さらにソフトウェアをメンテナンスし続ける限り、機能はどんどん増えていく。 迷いなく⾼頻度で検証を⾏うために、⾃動化できる検証は⾃動化して積み重ねていくことが重要。
⾃動テストの分類の定義
⾃動テストの分類を定義する必要性 中⻑期的にメンテナンスし続けられる⾃動検証の仕組みを作るため、⾃動テストの分類は必要。 しかしよく⾒るテストピラミッドの分類はちょっと誤解を⽣みやすい。 (タイムテーブルにもUnit Test, Integration Test, E2E Testと書いてしまいましたが……) 引⽤:https://saeedgatson.com/the-software-testing-ice-cream-cone/
Unit Test(単体テスト)と⾔うと クラス単体に対するテスト? という疑問が湧いたり Integration Test(結合テスト) の範囲はどこまで? という疑問が湧いたりする
本講演内での⾃動テストの分類の定義 引用: 開発生産性の観点から考える自動テスト( 2024/06版) https://speakerdeck.com/twada/automated-test-knowledge-from-sava nna-202406-findy-dev-prod-con-edition?slide=40 Small JVM単体でのJUnit Test 対象のクラスが1つか複数かは問わない
Medium データベースに接続するJUnit Test Large E2Eテスト テストだけではなく検証まで広げると、 「ビルドの検証」や「静的解析」が Smallより⼩さい検証としてあり それらも運⽤も重要。
ここから実体験の話
JVM単体のJUnit Test (Small size Test)
やった(やってもらった)こと テストの知⾒がある開発者の⽅が⼊ってきてくれて整備をしてくれた。 私⾃⾝はここで⾃動テストの素晴らしさを知り、たくさんJUnit Testを書くようになり、 その後仕組みの整備にも関わっていくようになった。 - ⽇次でテストを全件実⾏する仕組みを整備 - pre mergeで変更の⼊ったクラスのテスティングペアのテストを実⾏する仕組みを整備
- 同時にLinter, Formatterの実⾏も整備(これらはIDEでも実⾏) - テスト駆動開発の啓蒙(動画視聴、社内講演、ワークショップなど) この時⼀番重要だと感じたことをまず最初に話します。
20年以上の運⽤の中で誰もJUnit Testを書かな かったのか? 否。 今までもJUnit Testの実装はあった。この整備が⾏われる際にも存在していた。 しかしほぼすべてが有効活⽤されておらず、Failするテストも⼤量にあった。 なぜそんな状況になってしまっていたのか?
20年以上の運⽤の中で誰もJUnit Testを書かな かったのか? 否。 今までもJUnit Testの実装はあった。この整備が⾏われる際にも存在していた。 しかしほぼすべてが有効活⽤されておらず、Failするテストも⼤量にあった。 なぜそんな状況になってしまっていたのか? CIで実⾏されていないから CIで実⾏されないテストは腐る。間違いなく絶対に腐る。
何よりも先に準備することは 「最低毎⽇テストがCIで実⾏される仕組み」
JUnit TestをCIで⽇次実⾏する Small sizeのTestをCI上で実⾏することは全く難しくありません。 JavaでGradleを利⽤している場合であれば標準の test タスクを実⾏すれば良いです。 GitHub Actionsで毎⽇夜 9:30に
テストを実⾏するのであればこんな感じです。 DistributionやJavaのバージョンは 実際に利⽤しているものと揃える、 ⾮公開の内製ライブラリに依存する場合は 認証や通信経路確保などが必要になりますが、 それも難しい話ではありません。
JUnit TestをCIで⽇次実⾏する もう⼀つ重要なのが実⾏後の通知(特に失敗時) 誰も失敗したことを知らないのは 実⾏されていないのと同じ せっかくUnit TestがあるのにCIで実⾏されていない、 実⾏結果が通知されていない、 のであれば⼤変勿体無いことなので、 すぐに毎⽇実⾏して結果の通知を⾏うようにしましょう。
Pre mergeでの実⾏ 少なくとも⽇次でテストが実⾏されて結果が通知されていれば、テストが腐ることはありません。 しかしより早く検証の失敗を検知できるように Pre mergeにもテストを組み込むことができないかを検討しましょう。 Pre mergeとはマージされる前、という意味で、 Pull RequestやMerge
Requestが作成された際にテストを実⾏し、 失敗した場合はmainブランチにマージされるのを防ぐことができます。 これによって修正者他の開発者に影響する前に検証を⾏うことができます。 また修正者が間違いなく⾃分の修正によって失敗したことを把握できる点も利点です。
Pre mergeでの実⾏時に注意すること ただし実⾏速度に関しては注意が必要です。 Pre mergeでの検証が完了するまで、マージを⾏うことができないため、 実⾏時間が⻑くなるとそれだけ待ち時間が増えることになります。 我々のプロダクトではコードセットがかなり⼤きいため、 Pre mergeでのテストは変更が⼊ったクラスのテスティングペアの実⾏に⽌め、 並⾏してデイリーで全件のテストを運⽤するという運⽤になりました。
啓蒙活動 - テスト駆動開発の動画視聴会 TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング
https://www.youtube.com/watch?v=Q-FJ3XmFlT8 - JUnit の基礎知識の勉強会 - レガシーコードにUnit Testを書くためのテクニックの勉強会 レガシーコード改善ガイドの内容を実際のプロダクトコードに適⽤ https://www.shoeisha.co.jp/book/detail/9784798116839 - 外部講師の⽅を招いた全社向けの講演 開発組織全体の盛り上がりの醸成と経営層への単体テストの重要性の訴求
データベースに接続するJUnit Test (Medium size Test)
やったこと - ⽇次でテストを全件実⾏する仕組みを整備 - pre mergeで変更の⼊ったクラスのテスティングペアのテストを実⾏する仕組みを整備 - 開発者が開発環境でテストを実⾏できる仕組みを整備 そのために以下を⾏いました。 -
データベースにアクセスするテストとしないテストの実⾏を分離 - テストクラス内でDataSource、Connectionを取得する仕組みを整備 - 開発者やCIで利⽤するデータベースのDocker Imageを整備 - テスト時のデータベースのデータの扱いのルールを整備 データベースを扱うことで⾊々な準備が必要になり、仕組みも⼤がかりに。 CIでのテスト実⾏時には全てのテストが同じデータベースを使うため、データ管理の戦略も必要。 プロダクトの固有の事情によって異なる部分が多いものだと思いますが、 できるだけ根拠を含みつつ話しますので、⼀事例として聞いていただければと思います。
データベースアクセス層のテストの必要性 データベースアクセス層はフレームワークを使った最⼩限のものにして、 そこにはテストを書かないと⾔う考え⽅もあると思います。 確かにデータベースアクセス層に過剰なロジックを実装することは良くないと思いますが、 例えばデータの絞り込みをデータベースアクセス層で⾏うのは⾃然な実装です。 そしてそこには機能に関わるロジックがあるため、それを検証することは必要だと思います。
データベースアクセス層のテストの必要性 レガシーコードでまれによく出会う、責務の絞り込みやレイヤー分けが⾏われていない神クラスに 外側から包むテストを書いて保護し、その後リファクタリングを⾏うという場合にも利⽤できます。 安全にリファクタリング
仕組みの実装 - データベースに接続するテストコードはmain, test とは別のフォルダに置くようにする - 専⽤のアノテーション @DatabaseAccessTest を作成 データベースにアクセスしたいテストにはこれをつけるようにする。
- gradle に databaseAccessTest というタスクを作成する - CIや開発者の端末で動かせるデータベースのDocker imageを準備(後述) - テスト⽤のConfigurationでlocalhostのデータベースへの接続を取得する仕組みを整備 (Spring未対応のコードが多いのでConnectionやDataSourceを取得するUtilityも⽤意)
仕組みの実装 これで開発者の端末のIDEでは - @Test がついたテストはいつでも実⾏できる - @DatabaseAccessTest がついたテストはテスト⽤のデータベースを起動したら実⾏できる CI上では -
gradle test で @Test がついたテストが実⾏できる - gradle databaseAccessTest で @DatabaseAccessTest がついたテストが実⾏できる (CI上にデータベースを起動する必要がある) という状態になりました。 開発者ローカルで実⾏する際の使⽤感を変えず、かつ データベースに接続するかどうかで明確にCIでの実⾏を分離できるように、このような実装にしました。
テスト⽤データベースのデータ データベースに接続するテストで重要なのがテスト⽤データベースのデータ。 テスト前のデータベースのデータはパラメーター。 データベースにアクセスする処理の結果に影響します。 テストの冪等性を担保するため、安定している(毎回同じデータ状態である)必要があり、 かつテストケースとの関連が分かるようにする必要があります。
テスト⽤データベースのデータ テスト⽤データベース内のデータは「出荷データベースの状態」としました。 ※おそらく社内⽤語 - テーブル定義などのスキーマ情報 - 開発者が出荷した初期データ(設定や共通機構が動くためのメタデータが多い) これらが⼊っておりユーザーデータがない状態のこと。 これをDocker Imageとして利⽤し、毎回作り直すことで毎回同じデータ状態になるようにして
そこにテストクラスのsetUpでそのテストに必要なデータを投⼊し、テストを実⾏します。 この2つを合わせてテストの前提データとする
テスト⽤データベースのデータ この運⽤にした理由 - すでに管理している出荷データベースのDockerイメージがあった - 新しくテスト⽤のデータベースイメージを管理していくのは⼤変 - バージョンアップのお世話をする必要がある 管理するデータベースのイメージは極⼒減らしたいので出荷データベースを流⽤した。 -
テストの前提データはテストケースとの関連がわかるようにしたい テストクラスのsetUpでデータを投⼊すれば強い関連性を持たせられる。
テスト後のデータの後⽚付け テスト⽤のデータベースは⼀連のテスト実⾏が終わるまで使いまわされる仕組み。 他のテストに影響が出ないよう、テスト実⾏後に「出荷データベースの状態」に戻すのが望ましい。 戻さない場合 どんどん前のテスト⽤のデータが溜まっていってしまう
テスト後のデータの後⽚付け(理想) Spring JDBCを利⽤してDataSourceをインジェクションしているクラスでは @Transactional を付与すると、実⾏前に @Sql やsetUpで投⼊したデータも含め、 テスト実⾏中の変更をロールバックしてくれます。 Spring JDBCを使っており、
プロダクトコードでトランザクション制御を書いていない層にテストを書く場合はこれが楽ちん。 (新しいプロダクトはこうしました)
テスト後のデータの後⽚付け(現実) 20年もののレガシープロダクトの現実 - java.sql.Connection を直接利⽤している箇所が⼤多数 - トランザクション管理の⽅法や単位も統⼀されておらず、commit する箇所もまちまち 共通の仕組みでロールバックをコントロールすることが難しかった。
テスト後のデータの後⽚付け(現実) しかし仕組み化できていない状態でテストコードが増えていくと、 他のテストが残したデータ(や消したデータ)によって失敗するテストが出始めた。 他のテストの残留データ INSERT INTO USER (userId, userName) VALUES
(gengen0719, じぇんじぇん); 一意制約違反
テスト後のデータの後⽚付け(現実) 仕組みでコントロールすることが難しかったためルールを作った - ユーザーの操作で起きえない変更はしない ⇒テーブルのDROPや初期状態で⼊っているレコードの削除はダメ。 - ユーザーの操作で起きえる変更は吸収できるように実装する ⇒Userテーブルのデータはユーザー操作で追加されうるので、消してからINSERTする ルール⾃体は定着し、⽇次でテストが実⾏されるため問題が起きてもすぐに修正されている。
テスト後のデータの後⽚付け(現実) しかしルールにするにしても - テストで変更したデータは必ずもとに戻すこと もしくは - テストで変更したデータはできるだけもとに戻すこと。もとに戻していないことにより他のテ ストが失敗した場合は修正すること というような内容の⽅が今思えば良かったかもしれない。
啓蒙活動 - 外部講師の⽅を招いたプロダクト全体でのワークショップ データベースに接続するJUnit Testを取り上げてもらい、実際のプロダクトコードを対象とした テストを実装するワークショップが実施されました。(私以外の⽅が実施してくれました) プロダクト全体の盛り上がりの醸成が⾏われ、全員にこの仕組みが浸透しました。 ほとんどの⼈がJUnit Testを当たり前に実装するようになりました。 -
新たにジョインした⽅向けのワークショップを定期的に開催 ワークショップ参加者の⽅がトレーナーを引き継いでいく形で定期開催を続けてくれています
E2E Test (Large size Test)
E2E Testとは ユーザーの操作からレスポンスまでのEnd to End のソフトウェアの動作を検証するテスト。 Webアプリにおいてはユーザーのブラウザ操作をエミュレートして、画⾯へのレスポンスを結果とし て検証する。 アプリケーションが完全に動作している必要があり、忠実性は⾼いがコストも⾼いテスト。
やったこと - ⽇次でE2E Testを実⾏する仕組みを整備 そのために以下を⾏いました。 - データ投⼊専⽤の環境を作成(ユーザーや打鍵評価環境相当のもの) - データ投⼊環境で投⼊したデータでアプリケーション全体が動作するDocker Imageの作成
- Dockerで⽴ち上げたアプリケーションにE2E Testを実施する仕組みを作成 まだ実験段階で、⽴ち上げプロジェクトが継続中ですが、少しずつ対象の機能を広げています。 実装はCodeceptJS + PlaywrightでJavaの話ではないのでデータ管理に関する話だけ。
実装のポイント - ここでも⽇次以上で実⾏が⾏える仕組みにすることを最優先にした - アプリケーションを動作させるため、かなり多くのデータの準備が必要 - 忠実性の⾼いテストが⾏える仕組みなので、テストデータも忠実性の⾼いものにしたい 忠実性の⾼いテストデータにするためには アプリケーションの機能を使ってデータを投⼊する必要があった。 機能を使ってデータ投⼊する場合、
事前に登録したデータをスナップショットにしてDockerで利⽤するか、 テストケースの中で全てのデータ投⼊をするかの⼆択だった。 しかし対象のプロダクトは別のプロダクトで登録したデータを元に動作する箇所が多く 全てのデータを機能で投⼊するには別のプロダクトも動作させる必要があった。 そのためテストケースとデータの関連性は⼀定諦めて、スナップショットにする⽅法にした。
効能 - 外側からつつむテストを作ることができるのでリファクタリングを する前に良い - クラス単位の挙動ではなく、アプリケーションそのものの挙動の 正しい動作を積み重ねていくことができている と感じています
巨⼤なレガシーコードと対峙する際に ⾃動テストが与えてくれたもの
私が巨⼤なレガシーコードと向き合う時に最も苦しいと感じていたことは どこまでいっても終わらないという感覚でした。 不具合を⼀つ直して、次に別の機能の不具合を直して、また別の機能の不具合を直して、 そうして最初の不具合があった機能に戻ってきた時に、前回読んだコードの内容を忘れてしまって、 またやり直しになる。 そんなことを繰り返して何も前に進んでいないように感じることがありました。 ⾃動テストは巨⼤な既存コードを読み解いた記録を残すことができる地図になってくれます。 少しずつ実装を⾏い、確実に実⾏を繰り返すことで、 ⼀歩ずつ前に進んでいる実感を与えてくれたと思います。
おわり
アンケートへのご回答をお願いします! JJUG CCC Fall 2025 全体アンケート セッションアンケート