大企業向けビジネスの信頼を支えるために半年かけてゼロからユニットテストを充実させたら、開発者も幸せになった 工夫5選

初めまして、購買ソリューショングループ 運用・機能改善チームの稗田です。当社では自社で運営しているECサイト(モノタロウドットコム)から直接商品をご購入いただく他に、他社の購買システムと連携して商品をご購入いただくシステム(大企業連携システム)があります。こちらの大企業連携システムには多くのバッチ処理があるのですが、これまで自動テストがありませんでした。今回はバッチ処理の障害をきっかけに短期間でユニットテストを充実させるためにした工夫や学んだことをお話しします。

ユニットテストを作らなければいけないと思ったきっかけ

障害発生

大企業連携システムは他社の購買システムと連携している特性上、当社のエンタープライズビジネス部門の営業の方が連携先のお客様をサポートしています。 note.com

障害のきっかけはほんの数行のコード修正だったのですが、お客様に影響する障害を発生させてしまいました。営業の方がお客様に原因や今後の対応を説明することになり、修正の規模に比べて影響は大きいものになりました。

担当システムやチームの状況

状況や背景を話す前にモノタロウのシステムを説明すると、当社では自社で運営しているECサイト(モノタロウドットコム)から直接商品をご購入いただく他に、連携している他社の購買システムから商品をご購入いただくシステムもあります。こちらのシステムは社内では「大企業連携システム」と呼んでいます。

tech-blog.monotaro.com

モノタロウドットコムとの違いとして、連携先のシステムが複数あり、そのシステムを利用されている企業も複数あります。同じ連携システムを利用していても事業形態が企業毎に異なるため仕様が異なることもあります。また、ここ数年で急速に事業規模が拡大し案件の数や新しいメンバーが増えたため、既存のコードが規模にあわなくなりレガシーコードになりつつありました。

チームの1人として感じたこと

上記のような状況で私はモノタロウドットコムを開発するグループから、大企業連携システムを開発するグループに異動しました。障害が異動して2、3か月目で起こったこともあり、影響の大きさに驚きました。また修正内容との因果関係がよくわからず困惑もしました。

お客様やステークホルダーの信頼を取り戻すために

この時期は他にも何件か影響の大きな障害が発生していたこともあり、営業の方に再発防止を強く求められていました。そのため、喫緊の課題として再発防止策を検討し実行する必要がありました。

障害が起こった共通の原因として、「仕様の考慮漏れ」「テスト漏れ」があり、その他の背景には事業規模が大きくなりその対応のために改修を重ねた結果、システムの仕様を把握するのが難しくなったことがありました。

そのため再発防止策の1つとして自動テストを充実させることにしました。更に再発防止のため短期間でテストを充実させることが期待されました。

ユニットテストを短期間で作成するためにやった工夫

自動テストは以前から開発者レベルでは必要性が認識されていたのですが、後回しになっていた課題でした。なのでテスト対象になるバッチ処理にはユニットテストがありませんでした。そのため短期間でテストを充実させるためにはチームで分担して作業する必要がありました。そこでスケールできる仕組み作りと仕様をテストに落とし込める仕組み作りを考えることにしました。

工夫1: 外部協力会社の力を借りる

まず、スケールできる仕組み作りとして自社の開発メンバーだけでは人手が足りなかったので外部協力会社の力もお借りすることにしました。キックオフには外部協力会社の方にも参加いただき現状の課題と目的を共有しました。

工夫2: 課題や目的、ルールをドキュメントで共有する

資料には以下の事を書きました。

  • 課題と目的
  • テストコード実装時のルールと注意点
  • 開発環境の構築手順
  • レビュー観点
  • 参考資料

ただ、初めから完璧なものを目指さず、少人数で実装しながらドキュメントを充実させて行きました。複数人で並行して作業するようになった後もレビューなどで話した内容を書くようにしました。資料の更新は誰でも出来るようにしたので自然と充実していきテストの品質向上に貢献したと思っています。

工夫3: リファレンス実装を作って横展開する

テスト対象のコードはPythonで書かれていたため、テストのフレームワークはpytestを選びました。他のプロジェクトで既に導入されていて、自分も使った事があったのが選択の理由になります。また、pytestの便利な機能に関して使用制限は設けず、テストコードをシンプルにすることに貢献するなら積極的に使うようにしました。複数人でテストを書くとテストの品質がバラバラになるのが心配でした。どうすれば一定の品質を保ってテストを拡充させていけるか答えを持っていなかったのでまずは少人数でコードを実装しながら方針を固め、それと並行してドキュメントを作りました。

テストは不安ベースで書かれがちで、増やすのは簡単ですが減らすことが難しい傾向があります。そのため、効果が薄いテストが増えないように何のためにテストをするかを意識してもらえるようにしました。特にテストを書くことに慣れない開発者のためにテストの名前や説明文のフォーマットもある程度決めました。

また経験上、プロダクトコードでは良いとされる抽象度や凝集度が高いスマートなコードはテストコードだとわかりづらくなるようです。なので単純な処理になるようレビュー観点に以下の項目をいれました。

  • 具体的な値を使う
  • テスト間のコードの再利用は気にしない
  • 条件分岐は使わず、テストを分割する
  • for文などのループ処理は使わず、pytest.mark.parametrize※を使う

※pytest.mark.parametrizeサンプルコード

@pytest.mark.parametrize('status, expect', [
    # 商品状況、期待値
    (0, True),
    (1, False),
    (2, True),
    (3, False),
])
def test_can_buy_status(self, status, expect):
    """テストケース: 購入可能か判定する"""
    # 以下の処理でstatus, expectを使用

コードカバレッジをテスト品質の指標にするとカバー率を上げるためのテストが書かれがちなのでカバー率はテストが不十分でないかを知る参考にとどめました。カバー率を上げる事よりユニットテスト対象外のコードを把握しやすくし、対象外の箇所は別のテストでカバーするように設計していくことでテストの品質を改善していけることを大切にしました。テストの品質があがっていけばプロダクトコードの仕様が明確になり、考慮漏れによる事故を減らせるはずです。

他には、外側から見た振る舞いについてテストをしたかったので、テスト対象のメソッドはパブリックに制限しました。当社のレガシーコードではこの制限はテストコードを実装する難易度を高くする場面がありました。テストコードがわかりづらくなった時はその都度、実装者と相談しながらテスト対象を決めました。あらためてふりかえると上記の制限がない場合、実装の詳細に依存したテストが書かれていたと思います。テストを実装する難易度はあがってしまいましたが、テストの目的を見失わない指針として助けられたと思っています。

リファレンス実装がある程度固まった後、きっかけになった障害を検知できるか対象のモジュールに対してユニットテストを書いてみました。結果、障害の原因になった箇所でテストが失敗したので横展開していくことにしました。

工夫4: 品質に責任を持つ担当者をおく

仕様の考慮漏れによる障害の再発を防ぐため仕様をテストに落とし込める仕組み作りとして、テストの品質に責任を持つ担当者を決め、実装者がテストコードを書くことに慣れるまでレビュアーとしてすべてのプルリクエストを見ました。こちらはレビュアー不足のため結果的にそうなってしまった面もあるのですが、全体を見てテストの品質に責任を持つ担当者がいたことがプロジェクトにプラスになったと思います。

工夫5: 実装しやすい単位に分割しこまめにリリースする

対象を連携先毎に分類して優先順位を決めました。担当者を割り当てて準備が整ったプルリクエストからリリースしました。テストコードの実装は外部協力会社の方にお願いし、レビューは社内の開発者にお願いしました。参考になるテストコードが増え、実装方針が落ち着いてからは今後テストをメンテナンスしていけるように社内の開発者も全員1回はレビューするようにお願いしました。

テストの実装をお願いした外部協力会社には他にも案件をお願いしていました。テスト拡充より優先度が高い案件もあるため、最低2人を目安に人数の調整もお願いしざっくりとしたスケジュールを出してもらいました。書き方に慣れてきたのと比較的余力がある時期が重なって計画よりも前倒しでやりきることが出来ました。リリースのペースはざっくりと以下の通りです。

  • 前半(1~2か月) 1、2件/週
  • 中盤(3~4 か月) 2~3件/週
  • 終盤(5~6 か月) 3~5件/週

ユニットテストを充実させるために必要なこと

その1: 泥臭い作業を愚直にやる

コードから仕様や機能を拾い出す

仕様に関するドキュメントは整っていないのでコードから仕様を拾い出してわからない部分はGitのログから実装者に意図を聞きました。しかし実装した開発者がすでに退職されている場合もあり、そういう場合はいったん仕様としてテストを書くようにして、テスト拡充とは別で対応するために課題としてリスト化しました。

レビューで抜け漏れを確認する

テストが必要な仕様に対してテストコードがあるかをレビューするようにしました。テスト漏れがないか確認する時にコードカバレッジを参考にしました。特にIDEのカラーリングで見える化する機能が便利だったのでレビュアーに使用を推奨することにしました。

実装の詳細は意識しすぎない

レビュー時にはテストコードが実装の詳細を知り過ぎていないように、以下の点について気をつけました。

  • モックをあてすぎていないか?
  • プライベートメソッドに対してテストコードがかかれていないか?

プライベートメソッドにモックをあてないようにしました。また参照透過性が保証されている外部メソッドに関してもモックをあてない方針にしました。モックをあてることでプロダクトコードの条件分岐の制御が楽になりテストは書きやすくなります。しかし、モックをあてすぎることでテストの目的がぼやけてしてしまうと考えたためです。そのためプライベートメソッドに関してもパブリックメソッド経由でテストをするようにしました。

その2: 無理にテストをかかない

プライベートメソッドに対してテストを書かない制限はプロダクトコードの品質が低いと特に厳しく感じました。そこでテストを書くのが難しいプロダクトコードはそもそも大幅なリファクタリングが必要なコードであると割り切って考えるようにしました。

なのでリファクタリング時にテストコードが書き直しにならない範囲でユニットテストを書くようにしました。テスト出来なかった仕様に関しては、他のテストで確認をするようにしました。

テストピラミッドと取り組みの関係

ユニットテストを作成中にでてきた悩み

悩み1: 潜在的なバグの発見

コードカバレッジを参考に仕様を確認していくと、どんなパターンでも通る事が出来ないコードをいくつか見つけました。恐らく潜在的なバグですが実装者に聞く事が出来なかったので後日対応する事にしていったんそのままにしました。

悩み2: モックをあてる?あてない?

テストしづらいコードに対してメソッドをモック化したい誘惑は常にありました。ただ、前掲した原則(モックをあてすぎない。パブリックメソッドに対してテストを書く)を守るようにしました。

そのため迷ったらそもそも何のためのテストであるかを実装者と一緒に確認することにしました。大幅にテストを書き直してもらうことが数回ありましたが、原点に立ち返る事でお互いテストとプロダクトコードに関する理解を深めていけたと思っています。

また、テストの品質についてどうしても水準をみたすのが難しい場合はテストを書くのを諦めました。幸い諦めたテストはそれほど出ませんでしたが、インテグレーションテストでカバーすることにしました。

悩み3: レビュアーの負担

実装側を意識しすぎてレビューする側をあまり考慮しなかったため、実装者が増えるにつれてレビュアーの負担が大きくなる問題が発生しました。実装に慣れてスピードがあがったプロジェクト中盤はレビューが間に合わずにリリースを延期することがありました。レビュアーは実装者より増やすことが難しいので、プロジェクトの初期から実装を担当していただいた外部協力会社の開発者にレビューを助けてもらうようにしました。また、レビューの難易度はテストコードの実装の難しさに比例することも実感しました。プロダクトコードの複雑さは考えずに実装の担当を決めてしまったので、レビュアーの負担にむらが出てしまいました。

まとめ

結果報告

ほぼ全ての既存のコードに対してユニットテストを作成することが出来ました。Gitブランチ更新時にユニットテストが自動的に動くようにして、コードの品質を担保するようになりました。

プロジェクト開始時は1ページだったドキュメントは最終的には13ページになりました。その中には開発環境構築手順(外部協力会社含む)や実装時のモックのあてかた、レビュー時のIDEの使い方などプロジェクトを通して得た知見などもかかれています。

ドキュメントに途中で追加された実装ルールの中で特にプロジェクトに対するインパクトが大きかった事は、テスト対象の実装に依存し過ぎないようにするためパブリックメソッドをテスト対象にした事です。

こちらの実装ルールを決めた時はユニットテストの実装が難しくなる心配がありました。実際にテストの実装が難しい場面に何回か遭遇しました。その時は実装者と何のためのテストかを話し合いながらテストを書きました。結果的にテストに対する理解が深まり、テストの品質向上に繋がったと考えています。

やってよかったこと

コードの修正に対して心理的な安心感ができました。IDEで気軽にテストできるので動作確認などが簡単に出来るようになりました。お客様や営業の方からのお問い合わせ対応で動作を確認したい時にも時々使っています。 また、後から気付いたことなのですが、新メンバーの受け入れにも便利でした。テストが正常に動くことを目標に開発環境を構築をしてもらうと構築完了の確認が分かりやすく、またセットアップのどこかに不備がある場合もトラブルシューティングで役に立ちました。実装をお願いする時もテストに書かれた仕様や実際の挙動を参考にできるので説明が楽です。

取り組みで学んだこと

コードカバレッジの使い方

あまりカバー率にこだわらず品質の低いテストを見つけたり、レビュー時に考慮漏れがないかを確認するのがチームにとって無理のない運用だと感じています。IDEのコードカバレッジの機能はすごく便利でレビュー時に使うようになりました。

参考になるコードがあると他のメンバーへの導入が楽

大量にテストを書いたので、参考になるコードがだいたいある状態になりました。レビュー時に実装の説明がやりやすくなりました。ユニットテストの書き方について説明が不十分だったと思うのですが、既存のテストコードを参考にしてすぐに順応してユニットテストを書いてもらえるようになりました。(開発者のみなさん一緒にやってくれてありがとう)

賢いテストは読みづらい(DRYの誘惑)

より凝集度が高いコードが書けないかとテストコードの共通化などをつい考えてしまいますが、テストコードはプロダクトコードとは実装の価値観が違うようです。

コード量の削減によるメリットは小さく、コード量が増えても、1.テストデータの準備、2.テスト実行、3.結果確認の流れを守るほうが理解しやすく修正しやすいテストコードになりました。

新しい課題

ユニットテストの実装が難しくてテスト対象から外したコードがあります。それらのコードはテストピラミッドの考えに基づいて、インテグレーションテストやE2Eテストでカバーしようとしています。自動テストが充実できたらプロダクトコードのリファクタリングもすすめていきたいと思っています。

事業規模が拡大し、より保守性の高いシステムが求められています。今回はその取り組みのひとつとして、大企業連携システムのバッチ処理にユニットテストを作成したことについてご紹介しました。他にもテストの拡充が必要な購買ソリューショングループが開発するプロダクトがあります。また、テスト拡充以外にもまだまだ改善点や新規の設計・実装などが必要です。上記の取り組みに興味がある方はぜひカジュアルMTGに申し込みください。