テスタブルな社内ツール 〜 Try-CatchのCatchをCatchの中に閉じ込めるな | Resilire Tech Blog
挨拶と記事の概要
こんにちは、Resilireのエンジニアの濵田です。
今年に入ってから、弊社開発・CS向け社内ツールの実装に携わる機会が何度かありました。
プロダクトの発展に直接寄与するところではありませんが、人間が手でやりきるには辛い、大量かつ単調な繰り返し作業を自動化して業務効率化ができると気持ちいいですよね。
とはいえ、現実は厳しく...
- ちょっとした定型作業の自動化であれば規模も小さく、継続的な保守はあまり考えずに実装しがち…
- 成長中プロダクトの運用をフォローするために必要なツールであれば、規模はそれなりにあるものの一時的に動いていれば良いと判断してやはり保守を疎かにしがち…
- 社内営業/CS向けの管理ツール・プロダクトDB・社内ツール…など複数プロダクトの間に落ちている手作業を自動化するとなると、外部プロダクトのAPIを叩くことがメイン動作となるので、ツール独自の実装がそんなに無く、テストを書ける領域が少なくなりがち…
そんなこんなで、社内向けのツールにしっかりテストを書くということが徹底されていない・そこまでの工数を確保しづらいという状況が現実だったりしないでしょうか。
ちょっとした自動化ツールこそテストが輝く
しかし、これらは結局のところ言い訳に過ぎません。
ツールは、プロダクトほどチームとしての保守運用コストをかけていないし、使われる時にしか動かないので、時間経過とともに老朽化していきます。また老朽化したことに気がつけません。つまり、テストコードの恩恵を大変に受けやすい開発環境といえます。
ツール全体が外部APIの影響を受けやすいなら、「テスタブルなコードを書く」という基本が重要な局面ともいえます。
何が言いたいかって言うと,「ちょっとした自動化ツールこそテストが輝くんやで。」ということです。
この記事ではテストを書く際に活用できるちょっとした考え方のようなものも共有するので、ちょっと読んで啓蒙されていってください。
ツール概要
さて今回この記事で例にあげるツールですが、以下のような仕様だと想定してください。めちゃくちゃ単純ですね!
- 手元に取込用のCSVファイルを用意する
- 外部サービスのAPIを実行しながら、CSVファイル内のデータ整合性をチェック
実装 BEFORE_テスタブルではないが、とりあえず実装
こんな感じで実装しましょう。
func メイン処理 {
call csv読込処理()
call csv内容チェック処理()
call チェック結果出力処理()
}
func csv読込処理() {
if !ファイル存在チェック {
ファイルを配置するようメッセージ表示
処理の停止
}
call CSVファイル読込()
}
func CSVファイル読込(){
hogehoge
}
func csv内容チェック処理() {
外部サービスのAPIにGET投げる処理
CSVファイル内容のチェック
return チェック結果
}
func 外部サービスのAPIにGET投げる処理() {
try {
GET投げる処理
} catch {
失敗したときの処理
処理の停止
}
成功したときの処理
}
func チェック結果出力処理() {
hogehoge
}
テストを書こう
テストを諦めそうになる壁
今回の実装では外部APIへの問い合わせがツールの処理の大半を占めます。つまり、実行結果が外部APIの健康状態に左右されるわけです。
単に外部APIの仕様が変わったのであれば、ツール側も修正する必要があるのでテストが失敗することに意義があります。
ところが、実際の運用では以下のようなケースのほうが多いです。
- 外部APIがメンテナンス中
- 外部API側に不具合があり、報告後修正待ちである
- 外部APIの利用上限を使い果たしてしまった
ですが、上記を言い訳にテストを書くのを諦めるのは尚早です。
「外部APIに関わるところ"以外"を通過する単体・結合テスト」が書けます。…ただし、今のコードをもう少しだけテスタブルにする必要があります。
APIの健康状態に影響されないテストを書こう
今回の例のようなツールにおいて、「外部APIに関わるところ"以外"」の処理は以下のようになります。
①APIを蹴る直前までの処理
②APIを蹴り、失敗したあとの処理
③APIを蹴り、成功したあとの処理
①は、単純に各処理に単体テストを書けば確認できます。
特に②についてテストしておくことはかなり重要です。失敗したときに意図通り停止するか・ログが出るか確認しておかないと、実際の運用で「なんか失敗したけど有用なログが出てないのでなんもわからん…」となるからです。
でも、今の外部サービスのAPI呼ぶクラスのままだと②と③のテストを個別に書くことが不可能ですね。
func 外部サービスのAPIにGET投げる処理() {
try {
GET投げる処理
} catch {
失敗したときの処理
処理の停止
}
成功したときの処理
}
さて、ここでこの記事のタイトルを思い出してください。
そうです、テストから外部APIの影響を引き剥がすためには、失敗したときの処理をCatch節の中にそのまま書いてはそれぞれのテストが書けません。
実装 AFTER_テスタブルにする
func 外部サービスのAPIにGET投げる処理() {
try {
GET投げる処理
} catch {
失敗したときの処理()
処理の停止
}
成功したときの処理()
}
func 失敗したときの処理(){
hogehoge
}
func 成功したときの処理(){
hogehoge
}
これで、失敗したとき処理単体のテストが行えるようになりました。単体テストファイルを見てみましょう。
func TEST_外部サービスのAPIにGET投げる処理() {
hogehoge
}
func TEST_失敗したときの処理(){
hogehoge
}
func TEST_成功したときの処理(){
hogehoge
}
Try-CatchのCatchをCatchの中に閉じ込めるな(タイトル回収)
さて、よくよく考えれば、外部APIに影響を受けなくとも処理に失敗したケースについては予めテストしておいたほうが安心です。
そもそも今後このコードに手を入れなければいけなくなっている状況では、すでに何かに失敗しているはずだからです。
ここで紹介したごくごく簡単なツール例にさえ、「失敗後のテスト」が書ける余地はまだまだありそうです。
まとめ
処理に失敗したあとに何か処理したいときは、必ずそのテストを書くべきです。
意図通りログが出るのか、後続のフォロー処理は行われるのか、エラー箇所がわかりやすいか…
でも、失敗したときにやるべき処理が「本当に処理が失敗したとき」しか実行されない場所(たとえばtry-catchのcatch節)にしかなければ、単体でテストができません。
実際に失敗するテストを書くのが王道ですが、失敗処理が単体で動くよう独立させるというのも候補のひとつとして頭にいれておくと良いかもしれません。
エンジニア採用強化中
株式会社 Resilire (レジリア) では、サプライチェーンリスク管理クラウドサービスResilireの開発メンバーを募集中です。
まずは情報交換程度に気軽に話して聞いてみたいという方もウェルカムです!
Discussion