食べチョク開発者ブログ

食べチョクエンジニアによるプロダクト開発ブログ

テスト改善チーム活動の軌跡(総集編)とチームの進め方

みなさま、お久しぶりです。QA エンジニアのujeです。 今回、テスト改善チームの活動が一段落したので総集編としてこれまでの取り組みを振り返ります。 改善チームの始まりはこちらの食べチョクの自動テスト改善活動 〜これまでとこれから〜をお読みください。

現在、チームは目標達成を契機に 10 月末で解散となり、メンバーは食べチョク開発業務に集中しています。

私を含めて 8 名で食べチョクの開発業務も並行だったので、1 人あたり週に数時間の作業時間でした。 そのため、少しずつの活動にはなりましたが多岐に渡って遂行できたと感じています。

ここではその成果一覧とその時のブログ記事をまとめました。

成果

4 月〜10 月の半年間でしたが、形骸化せず目標を達成できたので上手くいったチームになったかなと感じています。 自分なりにですが次の項目では上手くいった要因について振り返って見たいと思います。

チームが上手くいったと考えている要因

  • メンバーが改善活動に対して意欲的だったこと

    • 改善した方が良い箇所という意見出しを実施してから、テスト改善について重要だと感じているメンバーに集まってもらったので意欲が高かった
  • ミーティングが意味あるものだったこと

    • ミーティングは「レビュー」→「レトロスペクティブ」→「プランニング」を短時間で進めて意思決定を重視した設計していた
    • メンバーからもらった意見は抽象化して議題にしてやった方が良いものはプランニングで取り入れていった
  • 目標の設定ができていたこと

    • フワッとしたまま進めないことを前提に、まずは目標を数値を出す部分を対応した
    • 数値の取得が可能になったら最終目標を設定した
    • 目標に対して毎週どれくらいで達成できるか週次目標を算出していた

筆者個人が特に重要視していた点

「チームを動かすこと」「チームが意思決定しやすいように自身が動くこと」の 2 点です。

冒頭でもお話ししたとおり改善チームは食べチョク開発と並行した改善チームだったのでみんな本来の開発業務があります。

そのため、そちらが優先となりどんどん作業が後ろ倒しになって形骸化してしまう といった結果にならないよう注意していたことを箇条書きにしてみました

  • 重要視していたこと
    • ミーティングの準備、ファシリテーションなどを担当
    • 必ずネクストアクションを作る
    • 数値目標を作る
    • 締め切りを決める
    • 次ミーティングまでに出してほしいアウトプットや資料を提示して記載してほしい場所に誘導する
    • 1 施策にたいしてなるべく担当を 2 名アサインする
    • 問題が提起された場合や上手くいってないと感じたら早めの変更をする(ミーティングや進まない要因の解決を図る)

振り返ってみると筆者自身は毎週何かしら運営関連するアクションを取っていたように思います。 一番良くないのは、動かないことで忘れられてしまうことだと思っていたので、Slack で細かく発信していたように思います。

最後に

正直、かなりメンバーの意欲的な部分に助けられたと実感しています。一番早く業務が進むときって心のどこかに安心感や連帯感があることだと思ってます。 また私自身も次も同じようにいくとは限らないです。新しい改善チームを運営する場合は、手を変え品を変え試していく意識を大事にしていきます。

改善が新しい改善を呼び、結果として開発やお客様にとって安定したサービスになるよう尽力していきます。

最後になりますが食べチョクでは仲間を募集しています。 ご興味がある方は是非、RECRUITからご応募ください。

テストのカバレッジをコツコツ上げた話

こんにちは、endo と kawabata です。 今回はプロダクトチーム内の自動テスト改善チームでコツコツカバレッジを上げた取り組みと振り返りのお話をしたいと思います。 ここでテストのカバレッジを上げているのは、RSpec でのお話になります。 テストのカバレッジを上げていこうというお話は、こちらの食べチョクの自動テスト改善活動 〜これまでとこれから〜のお話からきています。

ゴールを設定せずに活動するのは良くないので、10 月末までにカバレッジを 80%まで上げるということを目標に設定しました。 80%まで上がれば広範囲をカバーできているだろうというざっくりとした見立てです。 今回の取り組みでは、大きく分けて以下の 3 点を実施しました。

  • どのテストを追加するか決める
  • コツコツテストを追加する
  • 不要なコードを削除する

どのテストを追加するか決める

テストのカバレッジを上げていこう!というのはいいのですが、闇雲にやっても改善幅が見えません。 そこで、改善幅を見つけるためにRSpec 実行時のレポート情報をクエリで可視化するで現状の状態を可視化しました。

その結果、どこから着手するかが見えてきました。

RSpecのグループ毎のカバレッジ

  • Controller / Mailer / Jobs のカバレッジが全体よりも低い
  • 3 つのグループの中で Controller は実際のエンドユーザー側へのコンテンツの表示に関わる割合が多い

ということで、Controller のカバレッジを改善しようという話になりました。

では、どのファイルからやるのか?というところも闇雲にやっても改善が見えづらいです。 カバレッジが低いものから着手しようということで、カバレッジが低いものを抽出しました。 これでテストを追加する対象が明確になりました。

Controllerのカバレッジが50%以下のファイル

コツコツテストを追加する

目標達成までに対応が必要なファイル数を算出し、目標達成までの日にちから逆算して毎週各メンバーに割り当てました。 平均すると 1 人毎週 1 ファイル追加するペースで進めば目標達成となる計算です。 テストを追加する対象が決まりマイルストーンも設定できたので、あとはコツコツと追加していくのみです。

しかし、ここからが大変でした。 最初のうちは順調に進んでいたのですが、途中からペースが鈍化し思うように進まない期間もありました。 各メンバーの業務状況により、テストを追加するための時間が確保できないというのが大きな要因でした。 これに関しては優先順位の兼ね合いで仕方ない部分があり、解決策がないので「先週出来なかったので今週は 2 つ追加しよう」みたいな感じで進めました。

不要なコードを削除する

テストの追加と並行して不要な機能やデッドコードを削除する活動も進めました。 使われていない処理のテストを追加しても無駄になりますし、テストがない不要はコードを削除すればカバレッジも上がるだろうという想定でした。 その結果、想定通りカバレッジ向上に効果的でした!

不要コード削除後の50%未満Controller数

不要コード削除後のカバレッジ

最終結果

コツコツテストを追加したり不要なコードを削除した結果、全体カバレッジが 80%を超えました!

最終全体カバレッジ

Controller 単体でも 80%を超えました!

最終Controllerカバレッジ

目標も達成したので、自動テスト改善チームは解散となります。

振り返り

今回の活動を振り返ってみて良かった点と課題が見えてきました。

良かったこと

グラフによるカバレッジの可視化

日々テストを追加してもカバレッジの増加はごくわずかです。 成果が見えづらいとモチベーション維持も難しくなります。 カバレッジをグラフ化していたことで日々右肩上がりに増えていくのを確認でき、 活動のモチベーション維持にも役立っていたと思います。

設定した目標を達成できた

業務状況により進捗が少ない時期もありましたが、自然消滅せず目標を達成できたことも成果の 1 つだと思っています。 あまり進捗しない状況でも継続することの大切さを改めて実感しました。

課題

安定した作業時間の確保

上でも書きましたが、各メンバーの業務状況によりテストを追加する時間を確保できないという状況が発生しました。 今回の取り組みだけのことではなく、こういった改善活動では起こり得ることです。 これに関しては良い解決方法が見つかっていませんが、 定期的にミーティングを開催して進捗状況の共有を行なっていたのは、良かったと思っています。 (個人的にミーティングのたび『頑張って進めないと!』と感じ、モチベーション維持に役立っていました)

おわりに

80%を超えたカバレッジですが、日によっては 80%を下回っています。 日々システムが進化していてコードは増え続けていますが、それに対応したテストが不足している場合があるということですね。 カバレッジが高いから良いというものではありませんが、必要なテストが不足していることは問題です。 自動テスト改善チームは解散となりますが、プロダクトチーム全体としてテストを書くということを意識していきます。

目標達成後のある日の全体カバレッジ

デザイナー同士で行う3つのミーティング設計

こんにちは。食べチョクの松久です。

今回は、デザイナーの活動を支えるミーティングをどうやっているか紹介します。デザイナーが関係するミーティングは3つあり、それぞれのミーティングの目的を説明していきます。

プランニング・交通整理

月・水・金曜日に施策の整理をしています。1回 30分程度です。

デザイナーが行うことは、ユニットに所属する施策以外にも、マーケティングや広報など他の部署・組織と一緒に行う施策があります。そこで、施策の状況の確認や、新しい施策の話について確認する必要があります。

施策は全て、GitHub issue に記載され、GitHub Projects にある ボードで管理をしています。ボードを見ながら、進捗の状況や新しい施策の対応を決めます。

デザイナーのGitHub Projectsの様子

さまざまな施策があり、人数に限りがあるのでどうしてもアサイン(割り当て)をすることになっている問題があります。理想としては issue を読み、施策に自発的に取り組める状況が理想ですが、まだたどり着いていない、というのが現実です。

Designer Team Discussion

「Designer Team Discussion」という名前でカレンダーに登録していますが、堅苦しくはなく、ざっくばらんに話すための時間を、毎日1時間とっています。 話していることは下記のようなことです。

  • 取り組んでいる施策の UI について相談(雑なデザインレビュー)
  • デザイン界隈の話題( Figma のアップデートとか )
  • 最近の困ったこと、良かったこと
  • 雑な話

です。

色々な施策があり、1ユニットに複数のデザイナーがいるのは少ない状況なので、一人で抱え込まないように「定期的になんでも話せる時間があること」を大切にしています。そのため、これといって議題は決めていません。決める時もあるのですが、決めたら決めたで議題について、話せない時もあります。ここは改善の余地があるところです。

コミュニケーションの場でもあるので、デザインとは全然関係ない話になることもありますし、Figma などツールの話になることもあります。例えば下記のようなことを話しています。

  • 写真素材のダウンロード履歴が美味しそうな写真だらけで困る
  • いわき市に良さそうなゲストハウスがあるので、ワーケーションにいいのでは?
  • Figma でマスターデーターを管理し続けるのは現実的か?

などです。

このミーティングで話したことを「📝🍵🍡」という絵文字をつけて、Slack のデザイナーのパブリックなチャンネルにメモをしています。メモなのでツッコミは歓迎です。 ここで話したことから、業務の改善できそうなことを見つけて、取り組むこととして esa にまとめて、時々対応しています。具体的には、印刷物に関する資料をアップデートしたり、デザインシステムのアップデートをしたりしています。

Monthly Update

プロダクトチームと呼ばれる組織にデザイナーは所属しています。この組織では「Monthly Update」というミーティングが、月1回あります。この時に合わせて、この一ヶ月間で何を取り組んだか?確認しています。

このMonthly Update実施前にデザイナーで何を取り組んだのか成果を確認をするミーティングをしています。何を取り組んだのか確認するのと一緒に下記のようなチェックリストを実施することにしました(議事録は esa で管理しています)。

- [ ] Figma のアカウントの確認
- [ ] 📝🍵🍡 のリスト確認
- [ ] スタイルガイドの進捗確認

Figma のアカウント管理・確認は定期的に行う必要があるので月一のイベントは都合が良かったです。

デザイナーの成果を確認するのですが、書き出してみるのは時間がかかります。時間はかかるのですが、自分たちで何をしたのか?どんな成果だっけ?を確認する時間は必要だと思っています。ただ、確認しきれないこともあり、改善が必要です。

おわりに

ご紹介したミーティング以外に、1on1 や、プロダクトチームの定例ミーティング、全社でのふりかえりなどがあり、ミーティングは少なくありません。 ミーティングは、効率的に目的を持って行えるのが理想です。ただ、デザイナーが複数人いる状態になってまもないので、デザイナー間でのコミュニケーションを目的としたミーティングをしています。そのため、全体として少し多めの状態が続いています。

今回、半年ぐらい運営した結果を書き出してみることで、変えたほうが良い点に気づくこともできました。

今後もいろいろな取り組みで、個々のデザイナーの力をより発揮できる組織になることで、「食べチョク」の改善に向き合っていきます。

デザイナーへの依頼数を GitHub CLI と jq を使って集計する

こんにちは。食べチョクの松久です。

今回は、デザイナーへの依頼数を GitHub CLI と jq を使って集計している話を紹介します。 どうして、依頼数を計測することになったのか、どうやって GitHub CLI と jq を使って集計しているのか、を順番に説明していきます。

組織の中でのデザイナーの位置付け

最初に、「食べチョク」を作る組織の中でのデザイナーと、エンジニア、プロダクトマネージャーの関係は下記の図の通りです。

2022年9月現在の組織図

「食べチョク」を作る組織は、チームトポロジーを原則としています。 2022年9月現在、「プロダクトチーム」にミッションを持った「ユニット」がいくつかあります。ユニットには、プロダクトマネージャー、エンジニア、デザイナーなどで構成されています。ただ、ユニットによっては、デザイナーがいないこともあります。詳しくは「食べチョクのプロダクトチームとチームトポロジー」を参照してください。

デザイナーは、ストリームアラインドチームに所属するのを基本にしています。デザイナー(職能)だけの組織はありません。 組織図には存在していませんが、デザイナー同士で集まって組織的に活動する場面も多くあります。理由は、「食べチョク」でのキャンペーンや、株式会社ビビッドガーデンの運営に必要なクリエイティブ作業も担当するため、組織的な対応が必要となるためです。

キャンペーン、バナー、広告の事例

具体的には以下の仕事があります。

  • キャンペーンページのデザイン
  • キャンペーンに関するバナー類の作成
  • ディスプレイ広告などの作成
  • 広報に関する画像
  • コーポレートサイトの運用
  • Tシャツ

などがあります。

2021年末頃、デザイナーも、マーケティング、広報担当も今より少ない人数で、なんとか対応していた状態でした。 その後、デザイナーの人数は増え、マーケティング、広報担当の人数も増えました。人数が増えた分、取り組む「食べチョク」や、株式会社ビビッドガーデンの運営に必要な「クリエイティブ作業」も増えて、対応が追いつかなくなってきました。

バナー作成など依頼数が増えてきたがどれぐらい増えたのか計測したい

本来であれば、依頼を受けて対応するよりも、一緒に目的のために活動できる状態( = ゴール)が望ましいのですが、まだその状態には達していないです。ゴールに向けて、今の状態を把握するための取り組みから始めました。

最初の取り組みは、Slack などで行われていた依頼を全て GitHub issues で残すことにしました。 プロダクトチーム(ストリームドアラインチーム)はもちろんですが、マーケティング、広報なども issue に書き起こしてもらうことを徹底しました。issue には、部署ごとのラベルをつけて分類しています。 issue は複数のリポジトリで管理されていますが、デザイナーが関わるリポジトリは限られているので(いまは 3つぐらい)、そこはあまり負荷になっていません。

GitHub CLI と jq を使って集計する

GitHub CLI ( gh )を使い、issue のデータを取得していきます。 issue のデータは、json 形式で取得できるので、jq コマンドで tsv 形式にしてダウンロードします。

$ gh issue list -s all -L 1000 --search "label:"design:UI","design:ASAP","design:PS","design:PR","design:service","design:marketing","design:biz"" --json "createdAt,closedAt,number,title,url,labels,state"| jq -r '.[]|[.createdAt, .closedAt, .number, .title, .url, .state, ([.labels[].name] | join("/"))]|@tsv' > issue.tsv

gh コマンドには、色々なオプションがあるのですが、ラベルを複数指定することが難しかったので、search オプションでラベルを指定して検索しています。 検索結果を jq コマンドに渡して、tsv ファイルにして保存しています。 この後は、tsv ファイルを Google Sheets にいれて、集計します。

集計結果を活用する

集計結果は、毎月、プロダクトチームで成果を共有する場で、プロダクトチーム外で起きたプロダクトのこととして数値共有を 発表して esa に残しています。 また、完了数や、時間の配分も他の集計結果と合わせてわかってきたこともあるので、業務の見直しに活用できました。

おわりに

組織が大きくなり、デザイナーが色々な場面で求められるようになってきました。 求められるのは、嬉しいですが、残念ながら人数の限界もあります。どんなことが今できているのか数値で整理することで、他のチームに理解を求めたり、活動を可視化できました。

今後もいろいろな取り組みで、個々のデザイナーの力をより発揮できる組織になって「食べチョク」の改善に向き合っていきます。

RubyKaigi 2022で"Mie Food Sponsor"をします。持ってけ楽しめ三重食材!

こんにちは。プロダクト開発チームの Engineering Manager 兼 人事の @hirashun です。

利用率No.1の産直通販サイト「食べチョク」を開発/運用するビビッドガーデンではリリース当初からバックエンドをすべてRubyで開発しており、ユーザーに迅速なサービス提供ができる恩恵を最大限にあずかっております。

この度RubyKaigiが3年ぶりのオフライン開催@三重県ということで、Rubyコミュニティーに何か恩返しは出来ないことか・・・と考えた結果、
ビビッドガーデンは"Mie Food Sponsor"として三重のこだわり食材を携えて協賛することにしました!

こだわりをもってRubyで開発を続けるみなさんに、三重の生産者の方々のこだわりをたっぷりお届けします!是非RubyKaigiで三重の魅力もお楽しみください。

三重のこだわりお届けします

オフライン参加の方だけでなく、オンライン参加の方にも楽しんでいただけるように、以下の2つのスポンサー内容を準備しました。

  • 日替わりで三重の食材を会場にて配布
  • 三重の特産品を抽選で毎日5名の方にプレゼント

それぞれ説明していきます!

日替わりで三重の食材を会場にて配布

「せっかく三重に来たんやで、〇〇食べてみ!」と言いたいところですが、昨今の情勢もありなかなか手軽に食べ物を提供することが難しくなってしまいました。
しかしそれでも生産者の方々のこだわりをRuby開発者に届けたい!そんな想いで「個包装」「その場で味わえる」「持ち帰り可能」という条件を満たす三重食材を厳選しました。

会場にて配布します。こちらがその一部です!

疲れた脳に染み渡る「無添加・無加工の天然はちみつ」

image.png (236.0 kB)

脳の活性化に効果があるブドウ糖・果糖・ビタミン類・ミネラルを多数配合し、消化吸収に手間がかからずダイレクトに体のエネルギー源になるはちみつが連日Ruby尽くしの皆様を強くサポートします。

採れたままのはちみつに一切加工を施さず瓶詰めすることで、風味や栄養価をそこなわず、はちみつ本来の魅力を楽しんでいただけます。

ぜひ一度、無添加・無加工の天然はちみつを味わってみてください。

舘養蜂場本店

こだわり抜いた味と香りでリラックス「希少かぶせ茶」

image.png (185.3 kB)

かぶせ茶生産量日本一の三重県。旨味・甘味成分が増加し、香りも良く深緑色でとても鮮やかな希少なかぶせ茶が一日の疲れを癒やすリラックスタイムを演出します。

新芽の本数を意図的に減らし、新芽1本当たりの栄養分が豊富になっています。 お茶の摘み取り2週間前に黒い覆いを被せていて、旨味・甘味が増加し、まろやかな味わいになります。手間はかかりますが、おいしいお茶作りのためには欠かせない作業です。

新芽のすべての部位(茎、芽、粉)が入った本物のお茶(荒茶)をお届けします。

かぶせ茶ファーム

私も欲しいです。
全員分は準備出来ませんでしたが、そこそこの数を準備しています。ぜひ楽しみにしていてください!

詳細の配布方法は追ってTwitterの「食べチョク開発部」アカウントにてご案内します。

三重県の特産品を抽選で毎日5名の方にプレゼント

その場ではお渡しが難しいけどこれだけは味わってほしいという三重の逸品をいずれか1品プレゼントします。
オンラインの方にもお届けできればと思い、今回はTwitterで応募、その後当選された方に食べチョクを通じて生産者さんより直接発送します!

一例を載せますが・・・

三重県南伊勢町のブランド真鯛『あなたに逢い鯛。』
肉の芸術・松阪牛

おわかりいただけただろうか。私も欲しいです。

3日間、毎日5名の方に当たります!ぜひご応募いただければと思います。
応募方法は「食べチョク開発部アカウントのフォロー」と「会期中に #rubykaigi と #食べチョク を付けてツイート」です。

twitter.com

RubyKaigiの感想、欲しい食材等、思い思いのつぶやきをお待ちしております。
※当選された方には食べチョク開発部アカウントよりDMをお送りさせていただきます
※対象者はRubyKaigiに参加している方とさせていただきます

CTO西尾より

先日CTOに就任した @nishio_dens のコメントです。

このたび「RubyKaigi 2022」にフードスポンサーとして協賛できることを、大変嬉しく思っております。

ビビッドガーデン(食べチョク)は、サービス開始当初からRubyを用いて開発を続けてきました。Rubyは私たちのようなスタートアップには非常に相性が良い言語です。
素早くサービスを立ち上げられること、直感的にコードを記述できること、開発に必要なライブラリやツールがひと通り揃っていることが魅力的な言語です。

私自身、エンジニアとしてのキャリアの多くをRubyとともに過ごしてきました。
Rubyは他のプログラミング言語に比べて自由度が高い言語です。この自由さが魅力であり、コードを書いていて楽しい気持ちになれる言語だと思っています。
複数のプログラミング言語を使ってこれまで開発をしてきましたが、私はRubyが一番好きな言語であると自信を持って言えます。

長い間お世話になっているRubyとRubyコミュニティーに貢献できることは感慨深いものがあります。
今回の「RubyKaigi 2022」はオフライン開催も予定されており、三重県の生産者のこだわり食材を会場にて配布いたします。

皆様とお会いできることを楽しみにしております。

prtimes.jp

おわりに

今まで様々な都市での開催を重ねるたびに、RubyKaigi運営の方々の「その都市ごと楽しんでいけよ」というメッセージを感じずにはいられませんでした。
この度、参加される皆様に三重のこだわりが詰まった食材を楽しんでいただくと共に、このような形でRubyKaigiのお手伝いができることを嬉しく思います。

RubyKaigiが終わった後も、三重の生産者の方々が作ったこだわり食材に思いを馳せていただけると幸甚です。
気になった方がいましたら是非三重の食材を覗いていってください。
https://www.tabechoku.com/areas/mie

ビビッドガーデンは「生産者のこだわりが正当に評価される世界へ」というビジョンのもと、一次産業の課題解決を推し進める会社です。
引き続きRubyをはじめとした技術を活用しながら、日本中の生産者の方々のこだわりがより多くの人に伝わるように、プロダクトを改善し続けようと思います。

ビビッドガーデンのことが気になりましたらこちらも是非ご覧くださいませ!
https://note.com/vividgarden_corp/

それでは皆様、当日スポンサーブースもしくはTwitterにてお会いしましょう!

食べチョクの自動テスト実行速度を2倍以上速くした

皆さんこんにちは、エンジニアの久保金田一です。

今回は、食べチョクの自動テスト改善チームが取り組んでいるテスト実行速度の改善についてお話しいたします。

自動テスト改善チームとは何か?について知りたい方は、以下のエントリーをご覧ください。

食べチョクにおける自動テストの現状

食べチョクでは、GitHub Actions のワークフローを使って、push をトリガーに自動テストを実行しています。食べチョクのリポジトリには System Spec までを含んでいて、E2E は別リポジトリとなっています。

メンバーが増えて体制が整ってきたこともあり、この 1 年くらいに実装した機能についてはテストがしっかり書かれていることが多いです。一方で、それ以上前に作られた機能については、まだまだカバレッジが低い箇所もあり、みんなで手分けして少しずつテストを追加しています。

不足していたテストの追加だけではなく、新しい機能の開発でもテストは増えていきます。これらのテスト追加に伴い出てきた課題がテスト実行時間の増加です。徐々にテストの実行時間が伸び、結果として CI 全体の時間が伸びてしまい、開発者体験が悪くなりつつあると感じ始めていました。具体的には、push してから merge するまで時間がかかる、push 後しばらく別の作業を実施してからテストの失敗を検知して前の作業に戻るので非効率、などの意見がありました。

このままでは開発効率が悪くなる一方なので、自動テストの速度改善をしよう!ということになりました。

CI 上でのテスト実行時間の計測と観察

単にテストが遅いと言っても、どこに問題があるかを明確にしないとどういった改善が有効か判断することができません。まずはボトルネックを突き止めるために、CI 上でのテスト実行時間の計測と観察をしました。

テスト実行時間をランダムで 20 回分集計してみたところ、最短で 11 分 44 秒、最長で 16 分 3 秒、平均で 13 分 17 秒かかっていました。

rspec_execution_time_before
改善前のRSpec実行時間

この全体の実行時間を「テスト自体の実行時間」と「ファイルの読み込み時間」に分けて確認したところ、「ファイルの読み込み時間」が全体の 30% 前後を占めていることがわかりました。 例えば、全体の実行時間が約 14 分だった場合では、「テスト自体の実行時間」が約 9 分 36 秒、「ファイルの読み込み時間」が 4 分 30 秒といった具合です。

Finished in 9 minutes 36 seconds (files took 4 minutes 29.8 seconds to load)
4846 examples, 0 failures, 15 pending

次に「テスト自体の実行時間」をさらに分析するために、実行が遅いテスト TOP30 を抽出してみました。

$ bundle exec rspec --profile 30

その結果、1 秒以上かかる example が 6 つあり、最も遅いテストでは 8.4 秒かかっていました。この 6 つテストだけで約 25 秒かかっており、改善の余地がありそうでした。

ただし、6 つのうち 4 つは System Spec で、この 4 つについては時間がかかるのも仕方がない部分もあります。

また、他のテストの実行時間は 1 秒未満だったので、全体の実行時間からすると 1 つ 1 つのテストを少しずつ改善しても大幅な改善は見込めませんでした。

ここまでの分析から次のことがわかりました。

  • 改善には「テストの実行時間の改善」と「ファイルの読み込み時間の改善」の 2 つの軸がありそう
  • 1 つ 1 つのテストの改善は、最初に取り組むべき改善策ではなさそう

速くするための改善策

実行時間の計測と観察の結果から、「テストの実行時間の改善」と「ファイルの読み込み時間の改善」の 2 つの軸が見えてきました。

まず「テストの実行時間の改善」については、テストを並列実行するという案が出ました。他社事例などを調べた結果、食べチョクでも並列実行することで大幅な時間短縮が見込めそうだったため、チャレンジしてみることにしました。

次に「ファイルの読み込み時間の改善」についてです。これは意図せず解決に至ったのですが、Ruby 3.0 系へのバージョンアップが有効でした。Ruby 2.7.2 から 3.0.4 にバージョンアップした結果、ファイルの読み込み時間が約 4 分 30 秒から約 60 秒まで短縮されました。

以上のことから、テスト実行速度の改善策としては「テストの並列実行」に取り組むことになりました。

テストの並列実行

テストの並列化は、主に以下のような実装をしました。

  • フロントエンドのテスト(Jest)とバックエンドのテスト(RSpec)の実行の分離
  • RSpec の実行を 10 個に並列化

Jest と RSpec の実行の分離は、ジョブを分けて並列実行にしただけなので、ここでは RSpec の実行を 10 個に並列化した実装について解説していきます。 以下は、RSpec 実行のジョブと、並列化をするためのファイル分割スクリプトです。

  backend:
    runs-on: ubuntu-latest
    needs:
      - backend-prepare
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        NODE_INDEX: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    env:
      NUMBER_OF_NODES: 10
    steps:
      ...buildステップ...
      - name: Exec RSpec
        run: |
          SPEC_FILES=$(./bin/rspec_splitter.rb --glob 'spec/**/*_spec.rb' --node-count "${NUMBER_OF_NODES}" --node-index "${MATRIX_NODE_INDEX}")
          bundle exec rspec --format progress --format RSpec::Github::Formatter --format RspecJunitFormatter --out "tmp/junit-rspec-${MATRIX_NODE_INDEX}.xml" ${SPEC_FILES}
        env:
          MATRIX_NODE_INDEX: ${{ matrix.NODE_INDEX }}
          ...その他のenv...
      - name: Rename coverage result
        ...カバレッジを送信するステップ...

rspec_splitter.rb

#!/usr/bin/env ruby
require "optparse"
options = ARGV.getopts("", *%w[glob: node-index: node-count:])
glob_pattern = options["glob"]
$node_count = options["node-count"].to_i
node_index = options["node-index"].to_i
SpecFile = Struct.new(:path, :index, keyword_init: true) do
  def group
    index % $node_count
  end
end
files = Dir[glob_pattern]
  .sort_by { |path| [File.size(path), path] }
  .reverse_each
  .with_index
  .map { |path, index| SpecFile.new(path: path, index: index) }
puts files.select { |f| f.group == node_index }.map(&:path)

まず、GitHub Actions 上で、ジョブの並列実行をするためには、Matrix Strategyを使用します。例えば、複数の OS や言語などのバージョンの組み合わせに対する Job を記述することが出来るとても便利な機能です。

ここでは、NODE_INDEX という変数にそれぞれ 0 から 9 までの値を保持しておきます。NODE_INDEX のみ定義しているので、ここで 10 個のジョブが生成されることになります。 fail-fastを false にしておくと、ジョブが fail しても他のジョブの処理が継続します。

次に、RSpec の分割実行です。テストファイルを分割し、各ジョブで実行するようにしました。テストファイルのサイズを取得し、並列にした時の実行時間が平準化するようにしています。

具体的には、テストファイルのサイズ比較で降順にした配列に対して、ファイルパスと配列のインデックスを持ったオブジェクトを作成します。配列のインデックス % node_count と NODE_INDEX を比較し、一致した NODE_INDEX のジョブ番号で実行するという実装にしています。

結果

テストの並列実行を実現した後に、再度テストの実行時間をランダムで 20 回分集計してみたところ、最短で 3 分 8 秒、最長で 7 分 46 秒、平均で 4 分 45 秒という結果になりました。 改善前の時点では、最短が 11 分 44 秒、最長が 16 分 3 秒、平均が 13 分 17 秒だったので、以前の半分以下の時間でテストを実行できるようになりました。

rspec_execution_time_after
改善後のRSpec実行時間

さらなる改善に向けて

テストの並列実行によりテストの実行速度は向上し、一旦は開発者体験も良くなりました。

しかしながら、プロダクトは日々成長し、機能もテストも追加されていきます。また、既存のテストに改善の余地があることも認識しています。

よって、テスト実行速度の改善についてはこれで終わりではなく、今後は以下のような地道な改善にも取り組んでいくつもりです。

  • 並列実行のチューニング
    • 今回並列実行は実現したが、並列数の調整や実行時間単位で分割するなどで効率を up できる可能性がある
  • テストの重複排除
    • どのレイヤーで何をテストするかなどを整理して、無駄に複数のレイヤーで同じテストをしないようにする
    • ロジックのテストを Request Spec のような高コストなレイヤーでやらない(必要な場合を除く)
  • 作成するテストデータを削減する
    • DB アクセスが関係ない部分のテストでは、FactoryBot で create せずに buildbuild_stubbedを使う
    • テストに関係ないデータがafter(:create)でたくさん作られがちなので、作らないようにする
    • 1 example につき 1 expect で書くとわかりやすいが、DB レコードが必要なテストは毎回レコード作成することになるので、RSpec の aggregate failures を活用して example を統合する

最後に

食べチョクでは仲間を募集しています。 ご興味がある方は是非、RECRUITからご応募ください。

RSpec実行時のレポート情報をクエリで可視化する

どうもはじめまして。 muryoimpl です。

前回のエントリ 食べチョクの自動テスト改善活動 〜これまでとこれから〜 で、自動テスト改善チームが発足したことを書きましたが、今回はその活動の中で実施した、RSpec による自動テストのカバレッジのデータ収集の自動化と、そのデータを利用した可視化について書きたいと思います。

これまではどう可視化していたか

食べチョクは Ruby on Rails で動いており、バックエンドの自動テストは RSpec を使って書いています。

テストカバレッジは定番の SimpleCov で計測して結果を HTML に出力し、テストケースごとの実行情報は RSpec JUnit Formatter を使って XML として出力して、GitHub Actions でそれらの情報を Code Climate に送信していました。

また、可視化という点では、以前ビビッドガーデン Advent Calendar 2021 で私が書いたエントリの画像のように、テストカバレッジを Google スプレッドシートに記録してグラフ化し、変化が見れるようにしていました。

これまでも現状のテストカバレッジは確認できてはいたのですが、自動テスト改善チームが発足したときのミーティングで以下のような意見が出て、改善することにしました。

  • テストカバレッジをクエリで見えるようにしたい
    • クエリを書いてデータを取り出せるようにしておけばいろいろな分析ができる
    • テストの足りていない箇所の洗い出しが誰でもできる
  • 手作業で記録するのは飽きた
    • 属人性や反映タイミングもまちまち
    • 自動化していつでも誰でも確認できるようにしたい

Redash で可視化できるようにデータを取り込めるようにする

食べチョクでは Redash を既に利用しているので、ここにデータを投入さえできれば後はクエリを書いてグラフ化するのも組み合わせてダッシュボード化するのも自由にできます。ではデータをどう準備するかということになるのですが、以下の戦略を採用することにしました。

  1. GitHub Actions でテストが実行されたら、SimpleCov で算出したテストカバレッジの情報をCSV化し、BigQuery に送信する
  2. GitHub Actions でテストが実行されたら、RSpec JUnit Formatter で出力した XML からテストの実行時間情報を取り出してCSV化し、これを BigQuery に送信する
  3. BigQuery のデータを Redash と連携する

3 については、BigQuery をデータソースとして Redash で扱う仕組みが既にあるので、1 と 2 の仕組みを用意すればよいことになります。

1 と 2 についても、元となる情報は既にあるので、必要な情報を読み出して加工し、CSV にするだけです。

1. SimpleCov が収集したテストカバレッジを CSV 化する

SimpleCov には収集したカバレッジ情報を出力する Formatter の仕組みが用意されています。この Formatter を独自に用意して CSV を出力します。

今回はテスト関連ということで spec/support 以下に simpelcov_bq_formatter.rb というファイル名で Formatter を配置しています。

format メソッドが SimpleCov から受け取る引数の result には、全体、ファイル毎、グループ毎にまとめたテストカバレッジの情報が詰まっています。これを利用して CSV を作ります。今回は全体のテストカバレッジ情報と、各ファイル毎のテストカバレッジ情報をそれぞれ出すことにしたので、全体のほうのファイル名は "All Files" という名前にしてレコードを作っています。

require "csv"

module SimpleCov
  module Formatter
    class BqFormatter
      ALL_FILES_NAME = "All Files".freeze

      def format(result)
        job_id = ENV["JOB_ID"]
        created_at = bq_datetime_format(result.created_at)

        CSV.open(File.join(output_path, "coverages_for_bq.csv"), "wb") do |csv|
          # 形式 (covered% は (lines covered / relevant lines * 100).round(2) )
          # job_id,branch name,created_at,group_name,filename,covered%,lines,relevant lines,lines covered,lines missed

          # 全体の結果 は一行として出力する
          csv << [
            job_id,
            branch_name,
            created_at,
            "All Files",
            "All Files",
            percentage(result.source_files.covered_percent),
            result.source_files.never_lines,
            result.source_files.lines_of_code,
            result.source_files.covered_lines,
            result.source_files.missed_lines,
          ]

          # ファイル毎の情報を一行として出力する
          result.groups.each do |group_name, source_files|
            source_files.each do |f|
              csv << [
                job_id,
                branch_name,
                created_at,
                group_name,
                short_filename(f.filename),
                percentage(f.covered_percent),
                f.lines.size,
                f.lines_of_code,
                f.covered_lines.size,
                f.missed_lines.size,
              ]
            end
          end
        end
      end

      def output_path
        SimpleCov.coverage_path
      end

      private

      # full path を app/**/*.rb 形式にする
      def short_filename(filename)
        filename.sub(SimpleCov.root, ".").gsub(%r{^\./}, "")
      end

      def bq_datetime_format(datetime)
        datetime.strftime("%Y-%m-%d %H:%M:%S")
      end

      def percentage(percent)
        percent.round(2)
      end

      def branch_name
        ENV["GITHUB_REF"]&.sub(%r{refs/.+/}, "")
      end
    end
  end
end

JOB_ID という環境変数は、GitHub Actions の github コンテキストの情報で構成されています。どの PR と紐づくのか判別できるように github.run_idgithub.run_number を使っていますが、GitHub Actions の Job を再実行しても これらは変更されないので、github.run_attempt を加えて一意性を出しています。

RSpec の実行は並列で行っているので、扱いやすいようにカバレッジの結果を 1 つにマージします。GitHub Actions の step として、SimpleCov の結果をマージするための設定を書いた Ruby のファイルを実行して集約しています。このときに、先ほど登場した環境変数 JOB_ID を設定して渡します。

jobs:
  ...
  report:
    ...
    steps:
      ...
      - name: Merge coverage files
        env:
          RAILS_ENV: test
          JOB_ID: ${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
        run: bundle exec ./bin/merge_simplecov_results.rb
      ...

bin/merge_simplecov_results.rb では出力先と複数の Formatter を指定しています。

#!/usr/bin/env ruby

require "simplecov"
require "simplecov-html"
require "simplecov_json_formatter"
require_relative "../spec/support/simplecov_bq_formatter"

SimpleCov.collate Dir["coverage/.result-*.json"], "rails" do
  formatter SimpleCov::Formatter::MultiFormatter.new([
    SimpleCov::Formatter::HTMLFormatter,
    SimpleCov::Formatter::JSONFormatter,
    SimpleCov::Formatter::BqFormatter,
  ])
  add_group "Forms", "app/forms"
  add_group "Services", "app/services"
  add_group "Decorators", "app/decorators"
end

これで SimpleCov の収集した情報を CSV として出力するようになりました。

2. RSpec JUnit Formatter で出力した XML から CSV を作成する

RSpec JUnit Formatter を導入して、RSpec を実行するコマンドに --out をつけてテストケース毎の結果を XML として出力しています。これを SimpleCov のときと同様に、GitHub Actions 上で読み込んで CSV に加工します。

先ほど出力した SimpleCov の CSV (coverages_for_bq.csv) を使うので、テストカバレッジの CSV を作成した後の step で実行するようにしています。

  - name: Merge coverage files
    env:
      RAILS_ENV: test
      JOB_ID: ${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
    run: bundle exec ./bin/merge_simplecov_results.rb
  - name: Create CSV file for BQ
    run: |
      bundle exec ./bin/export_spec_execution_time.rb
    env:
      JOB_ID: ${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}

XML の path を Dir.glob で取得して Nokogiri で読み込み、内容を CSV として出力しています。

以下の処理では、XML に含まれる spec ファイル名から対応する app のファイル名を算出するために coverages_for_bq.csv を読み込んだり変換したりしていますが、単純に結果を CSV に出力するためならば不要でしょう。実際、変換処理が長くなったので、ここでは省略しています。

#!/usr/bin/env ruby

require "csv"
require "nokogiri"
require "simplecov"
require_relative "../spec/support/simplecov_bq_formatter"

class SpecExecutionTimeExporter
  NODE_REGEXP = /\Ajunit-rspec-(\d+)\.xml\z/

  def initialize
    @xmls = Dir.glob(xml_glob_path).map do |xml_path|
      NODE_REGEXP =~ File.basename(xml_path)
      node_number = Regexp.last_match(1)
      [node_number, Nokogiri::Slop(File.read(xml_path))]
    end

    @csv = CSV.read(csv_path)
  end

  def to_csv
    CSV.open(File.join(output_path, "spec_execution_time_for_bq.csv"), "wb") do |csv|
      @xmls.each do |node_number, xml|
        suite = xml.testsuite
        suite.testcase.each do |c|
          app_fname = app_filename(c["file"]) # appファイル名

          csv << [
            job_id,
            app_fname, # appファイル名
            node_number, # node 番号
            filename_dict.key?(app_fname), # 該当appファイルの有無
            c["name"], # テストケース名
            c["time"], # 実行時間
          ]
        end
      end
    end
  end

  def filename_dict
    @_filename_dict ||= @csv.each_with_object({}) do |row, acc|
      app_filename = row[4] # filename
      acc[app_filename] = 0.0
    end
  end

  def job_id
    @_job_id = ENV.fetch("JOB_ID", nil)
  end

  def xml_glob_path
    File.join(SimpleCov.coverage_path, "..", "tmp", "junit-rspec-*.xml")
  end

  def csv_path
    File.join(SimpleCov.coverage_path, "coverages_for_bq.csv")
  end

  def output_path
    SimpleCov.coverage_path
  end

  private

  # system spec の app ファイルへの変換は諦める。
  # それ以外は 対応する app のファイルへの変換を試みる。
  def app_filename(spec_filename)
    return spec_filename.sub(%r{^\./spec}, "spec") if spec_filename.start_with?("./spec/system")

    name = spec_filename.sub(%r{^\./spec/}, "app/").sub(/_spec\.rb$/, "\.rb")

    -- ファイル名変換が長いため、 中略 --
    name
  end
end

exporter = SpecExecutionTimeExporter.new
exporter.to_csv

3. 出力した CSV を BigQuery に送信する

CSV ファイルを作成したので、後は BigQuery のテーブルにロードするだけです。 google-github-actions/auth と、google-github-actions/setup-gcloud を使って、認証し、bq コマンドで CSV をロードします。

steps:
  ...
  - name: Setup Google Cloud Auth
    if: ${{ success() && github.ref_name == 'develop' }}
    uses: google-github-actions/auth@v0
    with:
      credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}'
  - name: Setup Google Cloud SDK
    if: ${{ success() && github.ref_name == 'develop' }}
    uses: google-github-actions/setup-gcloud@v0
  - name: Publish Spec Report to BQ
    if: ${{ success() && github.ref_name == 'develop' }} # developブランチのみ記録する
    run: |
      bq load --source_format CSV tabechoku_dev_tools.tabechoku_rspec_coverages ./coverage/coverages_for_bq.csv
      bq load --source_format CSV tabechoku_dev_tools.tabechoku_rspec_execution_times ./coverage/spec_execution_time_for_bq.csv

全てのテスト実行の結果を取り込んでいるとさすがに煩雑になるので、default branch にマージされたときのみロードするようにしています。

Redash でクエリを作ってほしい情報を見られるようにする

データを取り込んでしまえば、後は SQL を書いてグラフ化するだけです。各種グラフを Dashboard 化していつでも結果を確認できるようにしています。

食べチョクでは、テストの実行時間、グループ毎のテストカバレッジに加え、テストカバレッジがしきい値より低い app ファイルの一覧を出力して、テスト追加対象のファイルが見えるようにしています。これにより、テスト追加対象のファイルと、テスト追加による成果の認知が容易になりました。

今回のデータを使った Redash の Dashboard
今回のデータを使った Redash の Dashboard

既存のデータをクエリで取捨選択、加工を可能にしたことで、単純なテストカバレッジや実行時間だけでなく、目的に沿った情報を自由に取得・表示することができ、有効活用できるようになりました。

今後は対応の成果差分がわかりやすくなるようにグラフを改善したり、通知を活用することでモチベーションが上がるかたちでテスト改善を進めていければと考えています。

まとめ

既存のテストカバレッジの情報とケース毎の実行情報を GitHub Actions 上で加工して CSV 化し、BigQuery にロードすることで、Redash と連携して現状を可視化、分析できるようにしました。

元々ある仕組みと情報を利用してデータの形式を変換してクエリを書けるようにしたことで、要求に合った情報を取り出すことができるようになり、可視化も容易になりました。このデータを活用しつつ、テストカバレッジ向上のモチベーションが上がるかたちでテスト改善活動を進めていければと考えています。

最後に

食べチョクを運営しているビビッドガーデンでは、一緒に働く仲間を募集しています。ご興味のある方はこちらの RECRUIT からどうぞ。