LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

テストの取り組み、あるいは体のよい車輪の再発明について

ジョブセンスのエンジニアをしている松下です。
コードレビューと大人の話し合い、若手から作りの相談を受け付けることが最近の主な担当業務です。

先日自分のチームが取り組んだプロジェクトは、古いバージョンのPHPで実装されているアルバイト応募機能を、Ruby 2.4に置き換えるというものでした。
応募というのはコンバージョンに直接的に関わるため、機能に様々な工夫が加えられる等で煩雑化し、コードも荒れ模様。 それに対し単にRubyポーティングするだけでは意味が薄いので、不要な機能の削減やコードクオリティの向上に力を入れたプロジェクトでした。

さて、コンバージョンに直接関わる機能ということは入念なテストが重要となってきます。単にエラーを出したくないというのはもちろん、フォームでは個人情報の入力もあるためです。
ここではそのテストにおける取り組みの1つを紹介いたします。

テストで楽をしたい

前述の通り、テスト対象はアルバイトへの応募機能です。
そして自分が実装担当した部分は、とある経路で応募フォームに来た場合に、そこにフォームに加えてアルバイトの詳細情報を表示するというものでした。

まずフォームの入力〜SUBMITのテストについては、手堅さが最も求められる上に自動化しきるのも難しいためリリース前にテスト項目書を作成しての手動テストを実施しました。
チーム全員でテスト項目を出し合い、相互レビューによって内容を担保します。 (RSpecによる単体テストと、CapybaraにFeatureテストはある程度記述した上でです)

次に考えるべきは、フォームと並んで表示されるアルバイト詳細情報表示のテストです。
その実装は、詳細情報の表示となると項目数が多く、またそれを適切にユーザに見せるためのViewロジックが必要となります。
単純なものでは時給額の値が1000の場合に「1,000円」と表示するようなものであったり(これはRailsのヘルパにありますが)、アルバイトの形態に応じて表記を変更するといったものとなります。

こうしたViewロジックは基本的にはDraperに寄せ、その単体テストを書くことで挙動の保証をしています。
しかし当然ながらクラスの単体テストで全てを担保できるわけではあいません。
結合レベルの話もありますが、Viewテンプレート側に多少ロジックのははみ出ることはありますし、またそもそものところでDBに想定外のデータが含まれていてエラーとなることもあります。

想定外のデータについて補足します。
ジョブセンスは、学生ベンチャーに始まって東証1部上場までしたリブセンスの急成長を支えたサービスです。 したがって、正規化されていない場合やジェイウォーク、バリデーションが仕込みきれていなかった等で特殊なデータがDBに格納されている場合も睨む必要があります。

しかし発生し得るパターンを確認し、網羅的にそのテストデータを作成するのは非常にコストがかかります。
また、これはView層なのでサービス改善による変化が起きやすく、テストの資産価値は相対的には低めです。

ではどうするか。
既存のアルバイトデータに対して網羅的にアクセスして確認するのみです。
幸い我々には10数万件のアルバイト実データがあります。 そしてリブセンスでは、そんな本番DBのデータを開発用DBへ日次で同期させる仕組みがあるため(事故防止で個人情報はマスキングをかけています)、開発環境でも網羅的なアクセスをすることが容易です。

これにより、「え、そんなデータのパターンがあってエラーになったの?!」という状況を未然に防ぐこととしました。
細かい表示内容を担保することももちろんですが、まず最優先で担保したいのは「そもそもエラーにならない」ことです。

ツールを考える

指定したリクエストパターンに対してガツガツとHTTPしてくれるツール、と言えばJMeterあたりが使えそうでしょうか。
が、自分は練度の低いエンジニアであるため、使い方を調べるところからスタートした結果、案外時間がかかるかも知れない…。
それであれば、やることは単純ですし手に馴染んだ言語でササっと書いてしまった方が早いしモチベーションも上げやすいと考えました。
また、DBアクセス、URL生成、HTTPリクエスト、結果出力と、1つのコード内でシームレスにできるのは楽でもあります。

自分のチームにおいてはRubyが一級市民ではありますが、パフォーマンスを考えると並列アクセスがしやすい言語の方が良いです。 Rubyでもできないことはないですが、せっかくなので他の言語を使いましょう。

というわけで、比較的手に馴染んでいて並列処理のしやすいClojureを選びました。
使い捨てのコードなので、後に誰かがメンテがするようなこともなく苦情も来ません。たぶん。 (このようにして業務の中で第二言語の練度を高めていくのは、個人的に好きな学習スタイルです)

実装する (体よく車輪を再発明する)

まずは雛形を作ります。

$ lein new app reqs

そしてproject.cljに依存ライブラリを記述した上で、REPLを起動してsrc/reqs/core.cljにコードを書いていきます。

HTTPリクエストにはclj-httpを用います。 {:async? true}オプションによる非同期リクエストをベースにしているので、明示的に並列処理は書いていないです。

(ns reqs.core
  (:require [clojure.core.async :as async]
            [clj-http.client :as client])
  (:gen-class))

(defn- fetch-jobs-from-db
  "DBから求人情報を取得する"
  [] ...)

(defn- job->url
  "求人情報からエラー確認対象のURLに変換する"
  [job] ...)

(defn- async-parallel-req
  "指定したURL群に対して非同期にがががっとリクエストする"
  [urls ch]
  (doseq [url urls]
    (client/get url {:async? true}
                (fn [_] (async/put! ch (str "OK " url)))
                (fn [_] (async/put! ch (str "NG " url))))))

(defn- async-parallel-req-with-log
  "リクエストしてその結果をシリアルに出力する"
  [urls]
  (let [ch (async/chan)]
    ;; 出力を一本化するためのgoマクロ
    (async/go
      (loop [i 0]
        (let [log (async/<! ch)]
          (println (str i ": " log))
          (recur (inc i)))))
    (async-parallel-req urls ch)))

(defn -main [& args]
  (->> (fetch-jobs-from-db)
       (map job->url)
       async-parallel-req-with-log))

fetch-jobs-from-dbのところは、自分はKormaを用いました。
ここで対象となるレコード数が多い場合には遅延シーケンスを使って少しずつDBから読み込むようにするのもアリでしょう。

さて、これで完成です。
$ lein runっと。

出力の一本化

printlnするところでgoマクロを使っていますが、これは並列実行処理が同時に標準出力を用いた場合への対応です。
この対応をしなかった場合には、概ねは以下のように表示されるものの

OK https://domain.com/path/to/123
OK https://domain.com/path/to/124
OK https://domain.com/path/to/125

たまに以下のようになります。

OK https://domain.com/path/to/126OK https://domain.com/path/to/127

OK https://domain.com/path/to/128

したがって出力処理を一本化して、表示が崩れないようにしています。

なお、今回は標準出力を使っていますが、出力先が同時書き込みできない類のものであればこのような対処が必要となります。

ちなみに起動が遅いのが気になるのなら

JVM系言語の宿命ですね。

コンパイルをしてDripを用いるのはいかがでしょうか。
Dripをインストールした上で以下のようにします。

$ lein uberjar
$ drip -jar target/uberjar/reqs-0.1.0-SNAPSHOT-standalone.jar

結果

上記のプログラムと約10数万件あるジョブセンスの求人データを用い、2件のバグを発見しました。

これは前職での経験ですが、テスト専門の業者に依頼したところ、社内メンバーの半分のテストケースで1.5倍のバグが検知されたという話がありました。
リブセンスにおいてはテストコードを書く文化が根付いているものの、それとは別の話として、テストが自動であろうと手動であろうと「いかに効果的なテストパターンを想定するか」は重要であると考えさせられます。

テストにおいても効果的なLazyinessを発揮していきつつ、またモチベーティブに仕事ができるよう心掛けていきたいです。