はじめまして。むらみんです。CADDi に入って最初に着手したコードを書く仕事で色々とハマったのでまとめておこうかと思います。
コンテキスト
CADDi では、顧客から受注した製品の製作フロー管理 (サプライチェーン管理といいます) を営業系 SaaS を用いて行っています。その中で、実際に部品の加工を行っていただくサプライパートナーへの発注は、SaaS の画面上に設置したリンクから Firebase 上にホストしたウェブアプリと Firebase Functions を通じて帳票発行を行う外部システムの API を呼び出すことで実現しています。
この Firebase Function で実現している API は、TypeScript で書かれた Express を用いた一般的な REST API です。
きっかけとなった開発案件
本来は顧客との営業活動管理を目的とした SaaS をサプライチェーン管理に利用することで、CADDi のオペレーションは様々なペインを抱えるようになりました。また、キャディがより強固な製造業のマーケットプレイスを構築するための技術的な基盤が必要だよね、という攻め寄りのニーズも生まれてきました。 (本稿では詳しく触れませんので、気になる方はぜひカジュアル面談へ!) そのために CADDi のソフトウェアエンジニアたちが現在取り組んでいるのが、サプライチェーン管理を行うシステムの内製化です。今回私が担当したのは、新システムが発注書帳票を発行する際に利用する Firebase 上の API の実装です。これは、旧フローでは現行利用している SaaS 上の発注情報 id が API の引数になっており、既存 API のシグニチャでは新内製システムが利用することが出来ません。そのため、従前のエンドポイントと同様の挙動を行う、異なるシグニチャを持ったエンドポイントを開発するというものでした。
どのような「技術的負債」があったか
ここまでで紹介した Firebase 上に構築された SaaS の機能を補完するシステムは、過去に CTO が「突貫工事」(本人談) で作ったものでした。 私が開発に着手しようとしたときに目についただけでも、以下のような点は課題だなと感じました。
- 必須項目確認 null チェック、不正な id の確認などが処理の中で各所に散らばっており、どのようなケースでどのようなエラーが返るかわからなかった。
- API の機能テストがなかった。 (ユーティリティなどのユニットテストはあった)
- 帳票システムの API を叩く部分はファサードオブジェクトになっていたが、テスト時にフェイクオブジェクトを差し込みやすいようにするなどを考慮した設計にはなっていなかった。
どう取り組むことにしたか
こういった状況を踏まえて、以下の方針で新しいエンドポイントを実装してゆくことを決めました。
- 既存の外部 API ファサードは流用
- Express に与える handler から新しく書く
- 項目チェックはなるべく宣言的な書き方で、1箇所にまとめる方法を模索する
- なるべく End to End でテストをする
最終的にどうなったかのサンプルコード
実際のコードはお見せできないのですが、今回の取り組みを盛り込んだサンプルコードを用意してみたので、気になる方はそちらも読んでいただきながら、以降の解説を書いていきたいなと思います。 https://github.com/caddijp/firebase-functions-testing-sample
具体的なソリューション
express-validator の導入
express の middleware として差し込むだけで宣言的なバリデーションが利用できるということで、ほぼ一択の選択肢と思い導入しました。 現実的な問題として、単純な必須チェックや値の境界値の確認の域を越えて、より業務的なバリデーションや相関バリデーションを導入しようとすると厳しい気もしますが、現状では間に合っているので、困ったらそのときに考えようかと思っています。
しっかりテストするためのフレームワークを揃える
jest はユニットテストを書くためにすでに利用されていたので、最低限のテスティングフレームワークとしては問題なしと判断。それ以外のライブラリとして、power-assert, supertest, firebase-functions-test への依存を追加することにしました。
power-assert に関しては説明は多く要らないかと思いますが、 assert(A === B)
と書くだけで、テストが失敗したときに豊富な情報が得られるようにしてくれるアサーションライブラリです。テスティングフレームワークの用意する expect スタイルや should スタイルのアサーション DSL に精通するコストを払わずに充分な情報量を得られるメリットはやはり捨てがたいですね。
supertest は http 通信を行うテストの実行を容易にしてくれるライブラリですね。宣言的でシンプルに記述ができる点が良いと思い採用しました。
firebase-functions-test はその名の通りで firebase functions のテストを書くためのライブラリです。firebase に設定する configuration に、帳票タイトル名のプレフィクスなどが設定されているので、テスト用にスタブ値を設定する必要がありました。
jest によるモックの導入
Scala や Java に親しく JavaScript に疎いエンジニアとしては、jest でモジュールのパスを指定すればモックを差し込むことができるという jest のモック機能はとても斬新でした。何でも出来てしまうので使い方を間違えないようにする必要はありますが、なるほど JavaScript 界隈では DI の機構などもそこまで必要ないのかもな、と思った瞬間でした。
何に苦労したか
bodyParser が動かない
express では body に JSON を受け取ったときに、JSON を JavaScript Object にパースするミドルウェアを差し込む必要がありますが、 firebase ではリクエストの Content-Type
に応じて自動でこの処理を差し込んでくれる機能があります。が、テストコード内でリクエストを打っても、 req.body
が undefined
にしかならない…。
Firebase SDK のコードを読めるだけ読んで見たのですが、この bodyParser の処理を差し込んでいる場所が見当たらず、firebase のプラットフォームに載せないと動かないパーツなのかな?と推測せざるを得ませんでした。(教えて詳しい人!!)
解決策としては、普通に express で bodyParser を挟み込んでも問題ないので、あえて明確に追加する ことで妥協することにしました。
mock の書き方が難しい!
jest での mock は一通り function mock も ES6 Class Mock もやりたいことが意図通りに動くところまで持っていきましたが、書き方は結構煩雑で難しいなぁと感じました。これは慣れていくしか無いんですかね。もし、もっと簡素な書き方があったら知りたい! 改めて、JavaScript にはクラスという概念はなくオブジェクトがすべてのオブジェクト指向言語なんだなぁと実感しました。
express-validator もうちょっと小さくテストできないのかな?
express-validator のテストは、バリデーションに引っかかるリクエストをエンドポイントから流して、レスポンスを検証するという形で実施しました。
これでも特段問題は無いのですが、バリデーションの箇所だけコンポーネントを切り出してテストすればもうちょっとコンパクトになるのかな?というのは検討しつつ断念。express のミドルウェアのテストをするために Request
や Response
のフェイクオブジェクトを用意することのほうが大変そうだったので、その手法は取らないことにしました。
なるべく小さくテストすることにこだわりすぎるのも良くないので、今はこれでも良いかとは思っていますが、もう少し良い方法があったら知りたいところです。
感想
私自身はTypeScript を書くこと自体には4年ほどブランクがあり、Firebase に至っては利用歴は皆無だったのですが、調べ物にちょっと時間をかけてしまったものの問題なく開発ができたので良かったです。このラーニングも組織のスキルの底上げを図る投資だと思えるのは良いチームだな、というも思いました。
今回手を入れたコードは CTO による「突貫工事」(本人談)であり、彼には事あるごとに「こんなレガシーコードを触らせてしまって申し訳ないです」と謝られていたのですが、実際に触ってみると可読性は十分あって、手を入れるのもさほど大変ではなかったのが正直な感想です。アキさん流石!という気持ちです。
宣伝
そんな CADDi はエンジニアを募集しています。興味をお持ちいただけたらぜひご連絡ください。