こんにちは。ソフトウェアエンジニアのくぼぴー(@kubop1992)です。 2023年9月1日より、カケハシのMusubi 開発チームにジョインしました。(もう半年も経っている!)。
初めてテックブログを書くので緊張しますが、昨年より大規模なリファクタリングを行い、その中でサーバサイドエンジニアとしての役割と、QAエンジニアとしての役割を担うことがあり、二足の草鞋を履いた超絶器用貧乏な私がどんなことをしたのか、ということを書いてみようかと思います。
リファクタリング概要
このリファクタリングがどの程度大規模かというと、
カケハシのプロダクト内のメインどころとなる機能のサーバーサイドをまるっと別のリポジトリに切り出し、 APIを新しくした上でフロントエンドを500files以上変更した上で、さらに動作やデータを既存挙動と寸分違わず同じに、 そのデータを使った後続処理の動作も既存動作を担保する。
みたいな感じです。
リファクタリング課題
リファクタリングに際して、以下のような課題がありました。
- フロントエンドに多くの変換ロジックがあり、サーバーサイドのデータの構造の変更によって重大な不具合が発生する可能性がある。
- フォームに入力された値と、現在のデータの差分を検知するロジックが複雑であり、ユーザーが変更していないのに誤検知する。
- サーバサイドでのデータ型の解釈、フロントエンドでのデータ型の解釈が乖離する場合があった。
- コア機能であるため、影響範囲が広範囲に渡り、データを扱うロジックがフロントエンド・サーバサイドに散っているため修正する度に影響すると思われる機能の再点検が必要となる。
- 詳細な仕様書が無く、また経緯などの情報が散らばっているため、正しい仕様や影響範囲を認識して修正することが難しい。
...改めて書くと、凄いことやってるな...と思います。
エンジニアとしてもQAエンジニアとしても、ここまで大きなリファクタリングをしたことがなかったため、正直さまざまなところで戸惑うことがありました。今まで本や勉強会で得た知識をフル活用し、時には状況に柔軟にアジャストさせる必要がありました。
エンジニアとして行ったこと
- DDDを用いたアーキテクチャを設計
- 単体テストケースのネスト化
- Datadog・Sentryを用いたログの設計
- 外形監視アラートの作成
- 認証認可の設定
- SonarCloudを用いたカバレッジの取得
DDDを用いたアーキテクチャを設計
新しいバックエンドリポジトリのアーキテクチャを作成しました。 これまではディレクトリ構造や、どの部分に何を置くべきかが参入障壁となっており、 新しいメンバーにとっては高い壁になっていました。
オニオンアーキテクチャを提案し、チーム内でどの層に何を書くべきか、書かないべきか、という議論が出来る状態を目指しました。(サンプルコードなど書いた)
また、リポジトリはインターフェースでとり回せるようにDI出来るように実装しました。
- Controller | - UseCase | - Domain | |- Domain Object | | |- Value Object | | |- Entity | |- Domain Service | |- Repository - Infrastructure ...
単体テストケースのネスト化
単体テストが読みやすくなるように、ネスト出来るよう提案・実装しました。 Pythonだと、RSpecのようにSpecBDDで書き表せないので、Classによるネストで表現しました。
- 単体テストケースをClassを使って階層化する。 - 第一階層のClassを`Test_対象のメソッド名`とする。 - 第二階層以下のClassを`Test_#{前提条件}の場合`とする。 - テストケースの名称を`test_期待値内容`とする。
以下は一例
Datadog・Sentryを用いたログの設定
新規リポジトリということで、ログ周りの初期設定をしました。 また、その際はセンシティブデータが露出しないようマスクをする対応をかなり厚めにしています。(この部分は取り返しつかないので、超頑張った) このあたりはTerraformでの権限管理や、AWS Secrets ManagerあたりのCDKでの扱い方がネックでした。
また、一部ログについては「流しすぎコスト問題」があったので、サブスクリプションフィルターのPatternを設定、Kinesisに流し込む流量を減らすように対応しました。 AWS周りのこの辺りはあんまいじったことなかったので、めっちゃ勉強になりました...。
また、外形監視アラートの設定を以下のような観点で作成し、監視するようにしました。
- レイテンシー
- タイムアウト
- スループット
- エラーレート
- 401, 403, 404以外の40x
- 500
- 502, 503, 504
認証認可の設定
Cognitoを使った認証、ユーザーが特定組織に所属しているかといった認可を実装しました。 この辺りはすでに実装されている部分であったので、キャッチアップして移植といった感じです。
ここで仕組みを理解していたため、QAフェーズでこの部分のセキュリティテストはしなければな、と思えるようになったのでやってよかったです。
SonarCloudを用いたカバレッジの取得
CI/CDでSonarCloudを用いてカバレッジを取得できように設定しました。 結構あとあとになって対応したのですが、なんと94%の単体テストカバー率!! 単体テストおじさんなので、チームメンバーの品質意識の高さが嬉しかったです。
できなかったこと
- ロジックの実装・移植などは他メンバーに頼り切りになってしまった。
- 他メンバーが巻き取りまくってくれたのでとても感謝...!!!
- フロントエンドはほとんど触れずに終わってしまった。
- フロントエンドチームが優秀すぎて秒速で対応が進んでいた。
- インフラ部分が殆どで、一人作業になってしまった。
- 初期設計からどうなっているのかキャッチアップ出来ていない部分があった。
QAエンジニアとして行ったこと
- 現行仕様の整理
- 過去データの洗い出し
- 変換ロジック・データカラムの精査
- 開発同時並行のテスト実施
- 受け入れ条件の作成
- システムテストの作成
- 非機能テストの作成
現行仕様の整理
まずは現行、どう動くのか?ということがチンプンカンプンだったので調査・整理しました。 一体、どのくらい項目があって、どう動くのか!?というのを触りながらメモ、もう一回触って... スクリーンショットをとって、動画をとって、表にして図にして、こねくり回して頭に焼き付けました。
過去データの洗い出し・データカラムの精査
この機能では、DynamoDBを利用しており、その中でどのようなアトリビュートで、何が入っているのか。 過去にカラム変更はあったのか、あればどのように変更されたのか、というのを調査しました。
また、入力経路毎に異なるデータ型・アトリビュートで入っていないかをチェックし、 特定のユーザーの情報を開いた瞬間にクラッシュしないかどうかを確認しました。
それらの情報をまとめて、単体テストのデータに追加することで、過去経緯を含め情報が単体テストケースを見れば理解出来るようにしました。
変換ロジックの精査・単体テストケース追加
既存の単体テストや、コードを読み取り、ロジックを精査していました。 その際に、単体テストで足りていない部分を提案し、追加してもらってりしていました。
「うーん、今、コードのロジックとテストこんな感じ??」と以下のドキュメントを提出したら...
秒でエンジニアが以下の表を作って共有してくれて神だった。
受け入れ条件の作成
ある程度インフラ環境が整い、QAをどうしていくのか初期検討するタイミングで作成しました。
- データ取得
- データ書き込み
- ビジュアルリグレッション
- 過去データ読み取り
- 外部インポート
- 画面描画
- シナリオ
- 耐障害
- セキュリティ
- パフォーマンス
- ログ
- 監視
- CI/CD
- コード
- 過去不具合
が、リリース時にどのような状態になっているかどうかの指針です。 とにかく抜け漏れがないように影響範囲と確認するべき観点を考え、ざっくり作成しました。(どんどん改訂したり、古くなったりしたけど、最後の最後までチェックリストとして機能した。)
開発同時並行のテスト実施
チームでは、途中からスプリントゴールを設定していました。 例えば「開発環境でGETのAPIが叩けるようになる。」などです。
そのスプリントゴールに合わせて、何処まで出来ているかということが理解できるようになったので、 その範囲について全体的に動作確認を開発と同時に行っていました。
その数、なんと累計169件!
たくさん出過ぎ?
出ていいんです。最後に出るよりずーーーっと良いです。まさにシフトレフト! むしろ不具合が出ることによって、「ここってそもそも...」という正しい仕様を具体的に確認することが出来ます。
何より素晴らしかったのが、開発チームの応答速度の速さです。 不具合発見した際に、再現方法とログやコードのこのあたり?みたいなのを軽く書いておいたり、 Slackのhuddleで即座に集まって相談することでほぼ一両日中に修正してくれました。
まさに神速。速すぎて確認が追いつかないくらいでした。 しかも楽しい。確実に進んでる感じ、わいわい話しながら進めていく感覚が心地よかったです。
気持ちは勇者一行パーティ。 ボスがいる現地に先に行って調査した結果を報告して、倒すための作戦会議をして、誰が詳しくて即直せるかみたいなのを話し合って、方針を決めて「今日もよろしく!」と朝会が終わるのが楽しかったです。
不具合を調査、修正している中で、
「ちょっと最強のロジック考えたわ」 「マジ?」
みたいな展開があって燃えたりしました。
デザインや仕様についてもデザイナー、PdMに即連絡して確認しながら進めていました。
システムテストの作成
ある程度先行して調査をしたり、テストを実行していたので、影響範囲や機能については頭の中にマッピングされつつありました。 どこをどうテストしたら良いか?という知見が溜まっていき、どうやらこの機能では、バグが出やすい突くべきポイントというのが存在するということがわかりました。
それらを俯瞰的にみるマトリクスを作り、機能を網羅するテストを用意しました。 このマトリクスの項目は、日を追う毎にどんどん増えていきました。(笑
詳細は割愛しますが、例えばフォームの境界値や空白時の挙動、データベースの互換性やログ、共通の処理、インポート機能や削除の機能を網羅するようにしています。 超エッジケースはこのケースでなかなか見つけることが難しいですが、想定される挙動を影響範囲を明確にして9割くらい抑えられたかなというイメージです。
システムテストの実行は、開発チーム全員で行いました。 担当するシートを決めて、集中して証跡を残しながら実行していく、という流れです。 テストケースはLinterもないし、解釈によっては違う行動をさせてしまうかもしれないため、適宜相談をしながら行いました。 また、データベースの互換性に関しては、神開発者がDiffツールを作って大活躍しました。
非機能テストの作成
なかなか目の届かない部分ですが、非機能のテストも作成しました。 例えば、監査ログや行動ログ、認証認可のテスト、パフォーマンス・負荷テストなどです。 これらは、上記の受け入れ条件をもとにひとつずつ確認していきました。
考えていたこと・大切にしていたこと
- なるべく早い段階で不具合を発見し、品質保証が可能な状態になることを目指した。
- 組み立てたテストケースを流し、その範囲において不具合が「ない」ということを保証したかった。
- そのため、それらのテストケースがブロックされないように早期に検証して不具合を出した。
- 検証は少しずつ、協力し、良い雰囲気でを心がけた。
- 大量に不具合チケットを渡すというよりは、検証可能な範囲を見極めて少しずつ不具合を出した。
- 時に開発者のメンバーの意見を取り入れながらフリーで検証を行っていた。
- 不具合を見つけても、「今見つかって本当に良かった」精神。予防出来たことが尊い。
- 心折れそうな時も、QAは絶対折れずに諦めないでポジティブマインド大事。
- コードから離れても、同程度の会話が出来、修正内容も把握しておくこと。
- 実装の詳細までは追えなくとも、何が原因でどう対処したのかは聞いておく。
- もしかしたら別の部分に影響が出ているかもしれない可能性は頭から捨てずに置いておく。
- データ、過去データなど、機能の不具合として表出しないところをしつこく検証した。
- エンジニア・QAエンジニアの双方の視点を掛け合わせるようにした。
- 影響範囲は誰かに定義してもらうものではなく、自分で調べて相談して決める。
- どこからどこまでの機能に波及しているかどうかはコードやデータを見たり、協力して拾う。
- 日常の会話や、最近出た不具合から機能をマッピングして影響があるか既存機能を見て確認する。
- 絶対に不具合出したくないマインド。
- 可能性について考えてしまったら、それを実行して確認するまでやめない。
- どんなに流れを止めてしまう可能性があっても、不安がある限りアラートは出し切る。
- コストによって対応するかどうか判断する。
- とりあえずやってみるマインド。
- 見えてる範囲で大枠のプロセスだけ作って共有、詳細は走りながら検査と適応をして決める。
- 全然最初のものは間違えてる。でも形になってるだけマシ。直せばいいだけ。
- 考慮が漏れる場合もめっちゃある。次からそれ入れればいいだけ。
- パフォーマンステストなど、やり方が決まってないものに関してはとりあえず自分ができることを試してみる。
反省
- 振り返ると、影響範囲が極大すぎてしまっていた。
- 先に受け入れ条件を作っておいた方が良かった。
- チームで影響範囲を知れる・途中で見返せるので便利だった。
- デグレ予防のためにハッピーパスのE2Eは先に実装しても良かった。
- 途中からコーディングから離れてしまったので、コードの様子が見えにくくなった。(脳がパンクした
- 週次とかでコードSyncしても良かったかもなと思ったりした。
- なぜ発生したのか知りたくなっちゃう病。
- 不具合報告した後にデバッグして該当コードを突き止めようとして時間ロスした。
- ドキュメントの情報と新鮮度の大切さを身に沁みて感じた。無いとなにが正しいのかがわからない。
- テストケース作成に関しては経験や知識不足を感じた。
- 動く状態、というのを早めに準備できると良いかもと感じた。
- エラーが出るが、API生えてる状態とか、保存はできないが、画面が見えるとか。
どんな価値が提供できたか
- コードがある程度読めたので、不具合の原因を自分でも突き止められるため発見から修正までがスムーズ。
- チーム内にいて毎日話すってマジで大事。
- どの部分が、どのテストレベルに属するべきかみたいなことが話すことができ、細かい部分は単体テストケースに落とし込むことができた。
- 機能的なテストはもちろんのこと、非機能テストを作成することが出来た。
- 仕様や挙動が謎な部分は、コードからリバースすることでなんとなく理解することが出来た。
- データ構造や、裏側のロジックを知っていたので、叩けば良さそうなところがわかっていた。
- 異常な組み合わせなテストケースより、内部構造的に同じ意味なテストケースを省けた。
- インフラやログなど、あまり手がつけられなさそうなタスクを拾うことが出来た。
QAとエンジニアとこれから
今回、エンジニアもQAエンジニアも、同じプロジェクトで役割を実際に経験してみて、最初は、どちらも中途半端になるのではないかなぁと不安に考えていたのですが、コードが読めた上で品質について話し合うと、話が格段にスムーズだなと思いました。 また、同じチームに所属して開発しながら会話しているという点も毎日の差分を追えて良いなと思います。
機能的な問題についてはコードベースで話すのはもちろん、インフラ・非機能にも目が向き、検証の対象になることがエンジニア出身である強みかなと思いました。
まとめ
やっぱりチームで品質について考えるって最高にCOOL! アーキテクチャを考えて、コードを書きながら検証も出来て超楽しかったです。
開発チーム皆んなで協力してテストケースを実行したり、考えたりすることで、 チーム内に知見が蓄積していったり、プロダクトの品質を全員野球で守っていくぞ!という雰囲気になってとても良かったです。どんどんそういう文化が浸透しまくっていけるようにこれからも頑張りたい。
誰1人欠けても達成が難しかったプロジェクトで、品質についてガッツリ関わることができてとても良い経験になりました。