DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

レポート機能の計算結果が正しいことをどう保証するのか考えた話

こんにちは。DIGGLEエンジニアのhondaです。
開発メンバーの中では主にバックエンドを担当することが多いです。

今回はDIGGLEの根幹を支えるレポート機能の数値検証についてお話したいと思います。

DIGGLEのレポート機能

DIGGLEのレポート機能

DIGGLEでは取り込まれたデータを集計し予実管理に役立つレポートを自動で生成する機能があります。 データは単月、累計、部門、科目など様々な角度からリアルタイムに分析が可能になっています。
もちろん今後もDIGGLEの根幹の機能として進化を続けていく予定です。

レポート機能の検証に関する課題

そんな素晴らしいDIGGLEのレポート機能ですが、機能の検証に課題がありました。 自由度の高いレポートを作成できるがゆえに 条件の組み合わせはほぼ無限に存在し、レポートの数値の正しさを保証することが難しい状況でした。 もちろんモデルやコントローラレベルでのテストも存在していて、リリースのたびにきちんQAもおこなうのですが、
「本当にちゃんと計算できているだろうか」
「あれだけテストしたから大丈夫なはずだけど」
といった不安な気持ちが常につきまとっていました。

検証の方法を考える

検証作業の課題を洗い出すと以下のような課題が浮き彫りになりました。

  • 手動でテストをおこなうには時間がかかる
  • 条件の組み合わせが多岐に渡るため網羅的にテストすることが大変
  • 機能追加は随時おこなわれるのでテスト項目の管理に継続的にコストがかかる
  • モデルやコントローラーレベルのテストはあるが、ユーザーが直接見る数値に対する検証が薄い

これらの問題を解決するために検討したのが数値検証の自動化でした。
DIGGLEにはレポートの数値をCSVで出力する機能があります。この機能を使って出力されたCSVファイルに対する検証をおこなえば ユーザーが直接見る数値レベルの検証を自動でおこなえるので、工数削減が期待できそうです。
しかし、どうなっていれば数値が正しいと言えるのかということを考えるとなかなか難しいです。
用意したテストデータに対して期待する値が出力されることをテストするということも考えたのですが、 そのデータ自体の正しさや、データと期待する値のメンテナンスの工数などを考えると現実的ではないと判断しました。

入力データと正しい出力結果を簡単に準備する方法を考えたときに思いついたのが本番のデータを使うことでした。 本番のデータであれば多様なデータと多様なレポート形式、そして我々開発チームの血のにじむような努力によって支えられた確からしい出力結果が存在するのです。 もちろん、本番の出力結果が間違っていればテスト結果も間違えることになるのですが、完璧に正しいと言える出力結果が存在しない以上 本番の出力結果はかなり確からしい出力結果であるためメンテナンスのコストも考慮したときに妥当な選択肢と判断しました。

具体的な検証方法

以下のような手順で検証をおこないます。

  1. 数値検証用環境にリリース前のデータベースと同じデータの入ったデータベースを作成する
  2. リリース済みのブランチでこのデータベースに対して全てのレポートでCSV出力をおこなう
  3. リリース予定のブランチに切り替えてmigrationを走らせる
  4. 再度全てのレポートをCSV出力する
  5. 2と4の結果を比較する

これで既存のレポート機能に関しては様々な条件でデグレードしていないことが検証できます。 新規機能については残念ながら工数を抑えてデータの準備をおこなう方法が思いつきませんでした。(というかそんな方法はないんじゃないか?) しかし、既存分だけのテストを自動化しただけでもかなりの工数を削減できたはずなので今回はこれで良しとしました。

また、この検証をおこなうためには本番相当のデータベースの準備が必要になるのですが、 数値検証用環境は、安全のため外部からはアクセスできないようになっています。 今回は数値検証環境の説明については省略します。次回の記事で紹介予定なのでご期待ください。

データベースに保存された全てのレポートに対してCSV出力をおこなう

ようやくコードの話になります💦
やること自体は単純です。DIGGLEではレポートの条件がjson形式でデータベースに保存されています。 そのjsonをコントローラに対してpostするとcsvが返ってくるのでそれを全てのレポートに対しておこないファイルに書き出すということをおこないます。 数値検証はリリースのたびに実行する性格上Rakeタスクとして実装しました。
但し、通常のRakeタスクでは行わない以下のことをあえてやる必要があります。

  • Rakeタスクからコントローラに直接jsonをpostする
  • Deviseによる認証をスキップする

これだけ書くとやばいことをやってる感がありますがライブラリで用意されている機能を使うだけなのでご安心ください。
但し、テスト環境前提の機能なので本番環境等で使うことはおやめください

言葉で説明するよりコードを見たほうが早いと思うので実際のコードを元に簡略化したコードを載せます。 省略して書きましたが大体以下のような処理を行っています。

# wardenのテストモードを使う
# これでテスト用のヘルパーが使えるようになる
def use_warden_test_mode!
  Warden.test_mode!
  # CSRF保護設定を外す
  ApplicationController.allow_forgery_protection = false
  yield
  # rakeのプロセスに閉じているので変更しても影響は無いはずだが念の為元に戻す
  ApplicationController.allow_forgery_protection = true
  Warden.test_reset!
end

# リクエストするユーザーを偽装する
def next_request_user(user)
  Warden.on_next_request do |proxy|
    # Deviseを使っているモデルのインスタンスからスコープを取得する
    scope = Devise::Mapping.find_scope!(user)
    opts = { event: :authentication, scope: scope }
    # 認証されたユーザーを設定する
    proxy.set_user(user, opts)
  end
end

use_warden_test_mode! do
  companies.each do |company|
    # スキーマを切り替える
    company.exec_in_tenant do
      # 全てのレポートを閲覧できるユーザーでリクエストを投げる
      next_request_user(User.find_by(role: :owner))
      session = ActionDispatch::Integration::Session.new(Rails.application)
      status = session.post(url, params: parameter_hash, as: :json)
      # ファイルに書き出す
      write! session.response.body
    end
  end
end

それでは普段やらないであろう箇所について、簡単にどうやっているのかを補足します。

Rakeからコントローラを直接叩くにはActionDispatch::Integration::Sessionを使います。

api.rubyonrails.org

これでsessionを偽装し、コントローラに直接postすることができます。

session = ActionDispatch::Integration::Session.new(Rails.application)
status = session.post(url, params: parameter_hash, as: :json)
# ファイルに書き出す
write! session.response.body

Deviseの認証をスキップする方法はRakeの認証ミドルウェアであるWardenのtest_mode!を使います。

www.rubydoc.info

名前の通りminitestやrspecなどのテスト環境で使うメソッドですが、今回はRakeタスクで使います。 このメソッドを使うことでWarden.on_next_requestを始めとした様々なテスト用のヘルパーメソッドを使用することができます。 このon_next_requestを使用してリクエストを投げるユーザーを設定することで認証をスキップできます。 scopeやDevise::Mappingが何かという話はDeviseの仕様に踏み込むので今回は説明を割愛します。

# リクエストするユーザーを偽装する
def next_request_user(user)
  Warden.on_next_request do |proxy|
    # Deviseを使っているモデルのインスタンスからスコープを取得する
    scope = Devise::Mapping.find_scope!(user)
    opts = { event: :authentication, scope: scope }
    # 認証されたユーザーを設定する
    proxy.set_user(user, opts)
  end
end

これでRakeタスクを実行すれば、レポートをCSVに出力することができるようになりました🎉
この数値検証の自動化はすぐに威力を発揮し、バグの検出に役立ちました。

次回予告

今回はDIGGLEに数値検証を導入した経緯とその詳細をコードを交えてご紹介しました。 この数値検証を実行している環境がどうなっているかについては次回弊社の優秀なインフラエンジニアがご紹介いたしますのでご期待ください。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net