はじめに
最近、プロダクトの品質についてよく考えるようになりました。
これまでいくつかの会社でエンジニアとして働いてきましたが、負債が積み上がっていくプロダクトに関わることが何度かありました。画面の表示条件を正しいものに変えるだけ。フラグの分岐を1本足すだけ。そういう変更に、数週間かかってしまう。
ただ、そのとき一番堪えたのは、品質が低いこと自体ではありませんでした。急ぎで出したいものが出てきたときに、動けなくなるのではないか、という感覚です。せっかくいい機会が来ても、そこから大きく踏み出す余力が残っていない。
これ、自分がコロナ禍からずっとやってきた資産形成で考えていたことと、そのまま重なりました。お金の話に置き換えると、あの「数週間」が何だったのかを、自分でもうまく説明できる気がしています。
「数週間」の正体は、毎回払っている固定費
変更そのものは小さいんです。表示条件を直す、分岐を1本足す。コードの差分だけ見れば、たいした量にはなりません。
それでも数週間かかるのは、変更の大きさと関係なく毎回出ていくものがあるからです。自分がいた現場だと、こんな感じでした。
- 手動リグレッションの手順書を、頭から1周する
- 検証環境の数がブランチの数ほどないので、順番待ちをする
- リリース列車が来るのを待つ
確認そのものはすぐ終わることも多いんですが、環境待ちとリリースのタイミングは案件ごとに変わって、早いものでも1週間はかかっていました。差分が数十行でも数百行でも、ここは同じだけかかります。家計でいえば固定費です。家賃や通信費と同じで、その月に何をしたかとは関係なく、毎月きっちり引かれていく。
固定費が高い家計は、収入が増えても手元に残りません。プロダクトも同じで、実装が速くなったところで、世に出るまでの時間はほとんど変わらない。「簡単な変更なのに数週間」というのは、固定費だけを見せられている状態でした。
しかも、この固定費は放っておくと増えます。機能が1つ増えれば、リグレッションで確認する項目もその分だけ増える。減る要素がどこにもないので、時間が経つほど毎回の支払いが重くなっていきます。利息だけ払い続けて、元本が一向に減らない状態に近いです。
この3つ、性質はバラバラに見えますが、実は根っこは同じでした。それは後で書きます。
なぜ固定費が高かったのか
単体テストと結合テスト、E2Eテストの線引きがないまま、なんでも後ろのほうで確認しようとしていたからだと思っています。
表示の出し分けも、画面の振る舞いも、基本は全部そこでした。「この条件のときにこの項目が出る」みたいな、単体テストで十分に潰せるはずのケースまで、人が画面を開いて確かめる側に置かれている。しかもデータが変わりやすいせいで自動化しにくく、結局は再現性のない手動テストだけが積み上がっていきます。
そして、手で確かめられる範囲には限界があります。よく覚えているのは、PCとスマホで同じ画面がある機能で、スマホ側の表示を変えたつもりがPCにも影響してしまったケースです。手順書にはスマホの確認しか書いていないので、そのまま素通りしていきます。時間はかけているのにデグレは起きる、という一番割に合わない形でした。
念のため書いておくと、当時の判断が間違っていたと言いたいわけではありません。早く出すほうが正しかった局面は普通にあったはずです。ただ、その利息を毎回払っていたのが自分たちだった、というだけの話です。
先にコストを払っておく
自分たちのチームでは、この状況を変えようと決めました。単体テストで潰せるものは単体テストで潰し、その先のテストは必要最低限にする。自分ひとりではなく、チームで一緒に取り組んだことでした。
まとまった時間を確保したわけではありません。新しく触るところにはテストを書き、既存のところも触ったタイミングで足す。そうやってちょくちょく時間を取りながら、少しずつ増やしていきました。
上には、単体テストのほうで担保しているので結合テストは必要最低限にしたい、という説明をしていました。理解はされていた気がします。
やってみて効いたのは、テストが守ってくれる状態そのものでした。未完成のコードをmainに入れても、壊したらテストが気づいてくれる。この安心感があったから、Feature Flagで機能を隠したまま、他のブランチと同じタイミングでmainに乗せられるようになりました。
これが、地味に一番効きました。それまではブランチごとに環境を出し分けていたので、検証環境の順番待ちが発生していたんですが、同じ環境に複数の機能を同居させられるようになったことで、この順番待ちがほぼなくなったんです。リリース列車を待つ理由も同じです。機能はフラグの向こう側に隠れたままmainに乗っているので、列車が来るタイミングを気にせず出せます。
つまり、最初に払った投資は単体テストを書く時間だけだったんですが、それが安全網になってFeature Flagが使えるようになり、結果として3つの固定費全部が下がる形になったんです。プロダクト全体を変えるところまでは届かなかったんですが、チームの中では動作確認にかかる負担がかなり減りました。
ここでやっていたのは、要するに先行投資です。テストを書く時間は、今日のスピードを確実に落とします。それでも先に払っておくと、毎回の固定費が下がる形で返ってくる。
投資信託に回すお金も、同じ形をしていました。今日使えるお金は確実に減ります。それでも回しておくのは、そのぶん将来の選択肢が広がるからです。自分の場合は、これから先いろんな経験をしたいという気持ちが先にあって、そのためには資産があったほうがやれることが増える、と考えたのがきっかけでした。
ひとつ書いておくと、この効果を数字で測ったわけではありません。しかも同じ時期に、ブランチ戦略の変更もCIの整備もレビュー体制の見直しもやっています。そのため「テストを書いたから速くなった」と言い切れるほど、きれいな話ではないです。効いたのは別のところだった可能性も残ります。
それでも、テストがなかったら思い切った判断はできなかったと思っています。未完成のコードをmainに入れる、機能をフラグの向こうに隠したまま出す。どちらもテストが守ってくれるという前提があったからできたことで、なかったらもっと慎重に、もっと時間をかけていたはずです。
投資なので、元本割れもする
資産形成のほうでは、けっこう失敗しています。よく分からないまま不要な商品を買ってしまったこともあって、今思えば王道のやり方だけしておけばよかったな、と思っています。当時は暴落のニュースを見るたびに動揺していましたが、今はそこまで怖くありません。振り返ってみると、あれも経験のうちだった気がしています。
テストのほうは、書けば必ず返ってくるわけではありません。捨てることになった機能のテストは、そのままゼロになります。壊れやすいテストは、守ってくれる分よりメンテナンスの手間のほうが大きくなることもある。テストは置いておけば増える資産というより、持ち続けるのに手入れが要るもの、という感覚のほうが近いです。
ただ、お金ほど元本割れはしません。投資は判断を誤ると、払った分より減って返ってくることがあり、しかも簡単には取り返せません。テストは違って、たとえ悪い方向に転んでも、捨てれば書く前の状態に戻れます。下振れしたときに、取り返しがつくかどうか。そこがそもそも違う気がしています。
とはいえ、ここには落とし穴があります。テストに手入れが要るということは、テスト自体が新しい固定費になりうる、ということです。実装の中身に密着したテストを大量に持つと、リファクタのたびに全部直すはめになって、下げたはずの固定費が別の形で戻ってきます。だから、書くなら中身ではなく振る舞いに対して書く。同じ守ってもらうなら、維持費の安いほうを選ぶ、という感覚に近いです。
そのぶん、書かないという判断もしていました、と言えば聞こえはいいですが、正直サボっていただけのところもあります。ルールで決まっているわけでもCIで止まるわけでもないので、面倒なときはそのまま書かずに済ませてしまっていました。ただ、後々面倒なことになりそうなところや、書いておけば結合テストで確認する手間を減らせるところには、さすがにちゃんと書いていました。
全部に書く、ではないんです。家計で一番効果的に削れるところから手をつけたのと同じで、毎回の固定費が一番下がるところに書く。
お金では自然にできて、仕事ではできなかった
ただ、この二つが同じようにうまくいったかというと、そんなことはありません。お金のほうは、最初から見える化して始められました。仕事のテストで同じことをやり始めたのは、ずっとあとです。
お金のほうでまずやったのは、支出をすべて見える化することでした。マネーフォワードを使って、住宅、食費、水道・光熱費、通信費、趣味・娯楽など10項目くらいに分けて、毎月いくら使っているかを洗い出します。そのうえで、どこをどう変えればどれくらい貯まるのかを考えて、効果的なところから手をつけていく。習慣として定着するまでには数ヶ月かかりましたが、一度仕組みができてしまえば、あとは自動で数字が見える状態になりました。
なぜお金では自然にできて、仕事ではできなかったのか。自分ひとりで決められる範囲の違いもありますが、それより大きいのは、コストを払う人とリターンを受け取る人がズレていることだと思っています。
家計は、削った苦しさも貯まった嬉しさも、全部自分に返ってきます。テストは違って、書く時間を払うのは今日の自分で、返ってくるのは半年後のチームや、あとから入ってくる人です。同じ「先に払う」でも、自分の中で完結するかどうかがまったく違う。だから家計はひとりで始められて、テストの方針はチームで合意しないと動きません。
もうひとつ、削りすぎもよくありませんでした。食費を削りすぎた結果、筋肉量が落ちて体力まで落ちてしまったことがあります。数字ばかり追いかけていたら、体のメンテナンスが疎かになったり、人と会う機会が減ったりもしました。だから途中から、数値で追う支出と、追わないものを分けるようにしたんです。後者は体調のこととか、将来行きたい場所とか、お金には見えない目標です。
削っちゃいけないコストを削ると、あとになって取り返しがつかなくなる。単体テストを飛ばした結果デグレが多発したのも、たぶん同じ構造です。
続かない理由は、だいたい時間差にある
こうやって並べてみると、お金とプロダクトで共通しているのは、コストとリターンの時間差でした。
払うのは今日で、返ってくるのはずっとあと。真ん中の「何も返ってこない」期間をどう評価するかで、続けられるかどうかが決まる気がしています。
自分は高いものを買うとき、一括で家計簿につけるのではなく、月割りで少しずつ計上するようにしています。単月の数字だけで見ると、大きな買い物のたびに家計が歪んで見えてしまうからです。テストも同じで、書いた月のコストとして見れば、ただ開発が遅れただけに見えます。効いてくる期間全体で見て、はじめて割に合うかどうかが分かる。
最後に
正直、まだこの考え方を体系立てて説明できるところまでは来ていません。単体テスト・結合テスト・E2Eの棚卸しも、会社でやれる範囲のことはやったつもりですが、やり切れているかというと、まだこれからの部分が多いです。
ただ、最近はAIのおかげでやりやすくなってきた実感があります。個人開発では、カバレッジが100%になるまで単体テストを書いたり、抜けているケースを洗い出して足したりしています。以前の自分なら、そこまではやらなかったはずです。お金のほうも同じで、支出の傾向をAIに聞くだけで、ある程度の気づきが得られるようになりました。
これは、この記事で書いてきた前提が変わったということでもあります。テストを書く時間は今日のスピードを落とす、という話をしてきましたが、その今日払うぶんがAIでかなり下がりました。払うものが小さくなれば、割に合うと判断できる範囲は広がります。今まで「そこまでやる価値はない」と見送っていたところが、そうでもなくなってきている。
だから、もし何か一つだけ試すなら、棚卸しから入るのをおすすめします。自分の担当している範囲で、単体テストで潰せるはずなのに人が画面を開いて確認しているものを、一つ挙げてみる。それだけならチームの合意を取らなくても、ひとりで始められます。
みなさんは、自分の使っているお金と、担当しているプロダクトの品質を、同じ目線で眺めてみたことはあるでしょうか?まずは片方だけでも、一度見える化してみてほしいです。