esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

RuboCop オリジナル Cop 活用のオススメ

こんにちは、夜な夜なフォーチュンタワーに登っている nsgc です。

複数人でプロダクトやサービスを作る際に可読性や保守性を向上させるため、 コーディング規約を用意してフォーマットを統一したり、あきらかに不要な記述を静的解析ツールでチェックすることはよくあるかと思います。

Ruby でプログラミングしている場合、そういった Formatter/Linter としては RuboCop が有名ですが、 RuboCop 標準で用意されているルールだけではもの足りず、プロジェクトのコンテキストに特化した独自ルールが欲しい時がありませんか?

そんな時には、オリジナルの Cop の作成をオススメしたい!

tapp をチェックする Cop を作ってみよう

突然ですが tapp というツールをご存知でしょうか? tapp はメソッドチェーンの途中でもオブジェクトを表示できる優れものなのですが、プロダクトコードには入れたくありません。

コードレビューの時に「デバッグ用途の tapp が残ってますよ」 という指摘をしますが、本来はその前に CI で検知してほしいものです。そこで、今回 "tapp が呼ばれているか検知する" 独自 Cop を用意してみましょう。

まずは、Cop クラスの作成です。 RuboCop::Cop::Base を継承したクラスにコールバックを定義し、その中で問題になる条件がないかチェックし、最終的には警告メッセージを出すためのメソッドを呼びだします。

下記例では、メソッド呼び出し時に呼ばれる on_send コールバックを定義し、警告表示のための add_offense を呼んでいます。 RESTRICT_ON_SEND 定数に指定することで on_send で呼ばれる対象を tapp のみに限定し、add_offense で表示するメッセージを MSG 定数で設定しています。

# frozen_string_literal: true

module RuboCop
  module Cop
    module InternalAffairs
      #
      # # bad (ok during development)
      # # using tapp
      # def some_method
      #  do_something.tapp
      # end
      #
      # # good
      # def some_method
      #  do_something
      # end
      #
      class Tapp < Base
        RESTRICT_ON_SEND = %i(tapp).freeze
        MSG = 'Remove debugger'.freeze

        def on_send(node)
          add_offense(node)
        end
      end
    end
  end
end

Cop クラスが用意できたら、次にそのクラスを .rubocop.yml で読み込み、他の Cop 同様に有効にすると使えます。

require:
  - ./lib/rubocop/cop/internal_affairs/tapp

InternalAffairs/Tapp:
  Enabled: true

ここまで読んでどうでしょう?単純なメソッドコールの有り無し位ならとても簡単に作れそうですね。

もう少しリッチな構文チェックをしたい場合やオートコレクトを用意したい場合は公式で用意されている記事 が丁寧に書かれていますし、既存の Cop と近い Cop を作りたい場合は既にある実装 がリファレンスとして参考になります。

さいごに

私が参加しているプロジェクトでは、 特定のロールの利用者が操作時の証跡をデータベース上に残す必要があり、 controller でその証跡レコーディング用のメソッドが呼ばれているかを Cop でチェックしています。

また、Time::DATE_FORMATS で独自のフォーマットを用意しているのですが、それを用いないで書かれた場合にアラートをあげる Cop もいます。

チームのコードレビューで機械的に検出できるものを何度も指摘をしているなら、適応する Cop がないか探してみたり、なければ、自分たちで Cop を用意してチェックを任せ、人間にしかできないレビューに注力していきましょう!

価値創造契約10周年

アジャイル事業部で価値創造契約を担当している平田です。

価値創造契約という新しいサービスを発表して、また、最初のソフトウェアリリースから今年で10年になります。 価値創造契約についてはこちらから。(現在は新規受付は停止しています) https://agile.esm.co.jp/services/value_creating_contract/

詳細は上記のページに書いていますが、特徴を説明すると、初期費用0円とした上で、利用している期間だけ月額利用料をいただき、解約は自由というサービスモデルです。 これを発表した当時は、まだ今ほどアジャイル開発が受け入れられていなかった時期でもあり、「任せてくれたら大丈夫」という自信と覚悟を見せるためのご提案でした。 (そう思うと、請負契約ではなく、リスクをお客様側が持つ形での契約が一般的になっている現在は、隔世の感がありますね。)

価値創造契約はビジネスとして拡大はしなかったものの、ご契約いただいているシステムはいずれも長生きしており、継続的にメンテナンスをしながら使い続けていただいています。 一番長いものはリリースから10年になり、未だにリプレースすることもなく、運用を続けています。

ビジネスとして拡大しなかったのは、いくつも要因が重なったことが理由ですが、その理由のひとつに、我々が継続的に「保守」をやっていく体制をうまく作れなかったというものがあります。 通常、我々が取り組んでいるアジャイルなやり方では、ソフトウェアが動き続ける間、開発チームが継続的に追加開発や不具合修正を続けていきます。 一方、この価値創造契約のモデルでは、リリース後にメンテナンスは行うものの、開発チームを維持し続けるだけの費用をいただいていないため、チームは解散します。チームが解散してしまうことにより、知識の貯蔵庫が失われてしまうのが問題なのではないかと仮説を立てています。

今のところ、価値創造契約というサービスそのものを拡大していく予定はありませんが、上記のような課題を踏まえて、これまで通りの準委任契約での開発に引き続き取り組んでいくことはもちろんのこと、さらにその先をいくような契約形態やサービスを試行していきたいです。

ActiveRecord::LogSubscriber を使って追加でログを出力する

どうも muryoimpl です。

先日 ActiveRecord::LobSubscriber を使ってログ出力に手を加えたので、その意図と実装例をご紹介したいと思います。

ActiveRecord::LogSubscriber とは

ActiveRecord で発行されたクエリをログに出力する役割を担ったクラスです。ログレベルを debug とした場合に、ActiveRecord のクエリを発行した際に実行時間や SQL 文が出力されますが、それはこのクラスが活躍しているおかげです。

ActiveSupport::Notifications の仕組みを使ってログに記録する ActiveSupport::LogSubscriber を継承したクラスとなっていて、ActiveSupport::LogSubscriber の API ドキュメントをみると、ActiveRecord::LogSubscriber を使った例が記載されています。

ActiveRecord::LogSubscriber を継承した独自のクラスを作り、sql メソッドを上書き実装することで、既存のクエリのログ出力に加え、独自のログ出力を追加することができます。

なぜ ActiveRecord::LogSubscriber を使ったのか

今回私は、特定のテーブルのクエリが発行されているかどうか、発行されたときにどのくらいの時間がかかっているのかを確認したいがために、ActiveRecord::LogSubscriber を継承した独自クラスを作成しました。

Rails アプリケーションのログレベル全体を下げるとログの量が爆発的に増えてしまい、大量のSQLにより欲しい情報が埋もれてしまって探しにくくなってしまう問題があり、条件を指定してログ出力できないか?ということで、ActiveRecord::LogSubscriber の出番となったわけです。

この後記載する実装例でも出てきますが、SQL文やクエリに渡されたパラメータが payload として渡されてくるので、これらを条件にして出力する/しないの切り替えを柔軟に設定ができるのではないかと思います。

ActiveRecord::LogSubscriber 自体には、クエリが実装されているソースがどこかを出力するメソッドも実装されているので、あるテーブルへのクエリがどこから出力されるかを調べることもできるでしょう。

テーブルの特定の属性が更新された場合のみSQL文を出力する実装例

以下の環境で動作確認をしています。

  • Ruby on Rails: 6.1.3.1
  • Ruby: 2.7.3

ActiveRecord::LogSubscriber#sql の元の実装 や、ActiveRecord::LogSubscriber の API ドキュメント を参考にして実装しています。

今回は Profile モデルの memo 属性が更新された場合に、ログレベル info で SQL を出力するようにしています。ファイルは config/initializers 以下に配置しています。

class ProfileLogSubscriber < ActiveRecord::LogSubscriber
  def sql(event)
    self.class.runtime += event.duration
    payload = event.payload

    # debug レベルでログが二重に出力されないようにしている。
    # また、自分で実装したクエリ以外のSQLが出力対象とならないようにしている。
    return if logger.debug? || IGNORE_PAYLOAD_NAMES.include?(payload[:name])

    sql_str = payload[:sql]
    # profiles と memo が両方含まれているクエリのみを対象とする
    return if !(/profiles/i.match?(sql_str)) || !(/memo/i.match?(sql_str))

    name  = "#{payload[:name]} (#{event.duration.round(1)}ms)"
    name  = "CACHE #{name}" if payload[:cached]

    binds = type_casted_binds(payload[:type_casted_binds])
    # 親クラスの実装は debug で出力されるようになっているが
    # 今回は info で出力する
    info("#{name}  #{color(sql_str, sql_color(sql_str), true)}; #{binds}")
  end
end

# AcitveSupport::Notifications の"active_record" の namespace に
# ProfileLogSubscriber#sql を登録する
ProfileLogSubscriber.attach_to :active_record

event.payload は Hash で、実行されたSQL文(:sql)、SQLの種類を示す名前(:name)、SQL に bind する値(:bind_casted_binds) 等の情報が含まれています。これらを使って、出力するかどうかの判定と、ログに出力する文字列を作成しています。

ちなみに payload{:name] ですが、ActiveRecrod::Relation#explain を呼び出したときは "EXPLAIN"、テーブル情報等スキーマの情報を取得するSQLが発行された場合は "SCHEMA" になります。その他には、transaction を開始/終了した場合は "TRANSACTION"、定義したテーブルのレコードを操作した場合は "<モデル名> <操作名>" (例: Profile Load)になります。

以下は、前述のコードを config/initializers ディレクトリに配置し、development 環境のログレベルを info に設定した上で、bin/rails console を実行してクエリを発行した様子の画像です。profiles テーブルの memo を更新したときのみ SQL が出力されているのがわかると思います。

f:id:muryoimpl:20210426112809p:plain
rails console での実行例

さいごに

ActiveRecord::LogSubscriber を使って、従来のログ出力に加えて、特定のテーブルや属性に更新があったときのみログ出力する例を紹介しました。

特定の条件のときだけログを追加で出力したいといった場合に、Rails 本体のログ出力機能を書き換えることなく使えるため便利です。実行されたSQLの確認やチューニング、呼び出し元を特定する場面等で使えるので、こういう機能があったな、と覚えておくと活用できる場面に出会うかもしれません。

失敗するということはどのようなことか

突然ですが。 みなさんはソフトウェア開発でどれくらい失敗をしていますか?

失敗することについて、私見をまじえつつ考察してみたいと思います。

プロジェクトで失敗していますか?

仕事でソフトウェア開発をする場合、プロジェクトという単位で開発をすることが多いと思います。 プロジェクトは、その性質から基本的に一点物、その開発は常に初めて経験するものです。

初めてですから、成功も失敗もすべてを予想できるわけではありません。 解決したい領域が自分の不慣れなものであれば、どうすれば成功するのか予測することはいっそう困難なものになります。

これでは何かしら失敗をすることは約束されたようなものですから、心置きなく失敗してもよさそうなものです。

そもそも失敗とは?

しっぱい【失敗】

やりそこなうこと。 目的を果たせないこと。 予期した効果をあげられないこと。

三省堂 スーパー大辞林

プロジェクトで実現したかったことが、開発したソフトウェアで実現できなかったとき、失敗と言うことができそうです。 実現したかったことが実現できなかったのですから、これは本来はネガティブな結果です。

一方でよく耳にするように、失敗することでうまくいかない方法を見つけることができたのだから無駄ではない、という考え方があります。 この立場に立つと、うまくいかいないソフトウェアを開発するためにかけた資源をどう評価するかという問題は残るものの、失敗そのものは無駄だったというわけではありません。

はやく成功に到達するために、はやく失敗するというのは、変化の早い環境の中ではむしろ当然のものという理解も広まってきています。

失敗は、無理に取り除くのではなく、成功のための踏み台ととらえるのがよさそうです。

ただし、それとは別で区別したいものに、避けられたはずの誤りというものもあります。 思い違い、思い込み、確認不足によるミス。 これらはプロジェクトの性質によるものでなく、むしろプロジェクトへの取り組み方によるものでしょう。

失敗することとは別に、誤りをおかさない工夫はしておきたいものです。

いつ失敗するか?

失敗は避けるものでないとして。 では、失敗するならいつがよいのでしょうか?

これは先ほど棚上げした、失敗したことにかけた資源をどう評価するか、という話にもつながりそうです。

最初に考えられるのが、失敗が許される枠を用意して、その中で失敗をすること。 本番に影響を与えず失敗できるように、いわば練習期間を用意して、うまく行く方法やうまくいかない方法を見つけることに当てる。

わたし個人としては、これには異論もあるとは思いますが、自分の能力が資源であり能力を伸ばし効果的に発揮するという点で、プログラマはアスリートと変わらない部分があると考えています。

練習せずに本番にのぞむアスリートがいないように、プログラマにも結果の成否にとらわれない枠があってよいのではないか、と感じています。 個人が自分の能力を伸ばすことに努めるのはもちろんのこと、プロジェクトとしてもチームとしての能力を伸ばすための時間があってよいのかもしれません。

一方で。 できあがった時点では成功なのか失敗なのか、判断がつかないものもあります。 例えば、これはわたし自身が最近経験したことですが、よいユーザインタフェースに改善したはずが、逆にユーザに戸惑いを与え不必要な操作を増やしてしまう結果になってしまったものがありました。 実際にサービスを提供してみないと、何が正解なのか本当にはわからないものです。

ユーザに損害がおよぶ事態はさけなければなりませんが、時には実世界で成否をはかることも必要で、プロジェクトはそんな状況も加味しておくべきなのかも知れません。

うまく失敗できているか?

失敗とは何で、いつなら失敗できるか考えてみましたが、もう一つ他にも重要な要素がありそうです。 失敗することの得手不得手です。

正直なところ。 わたしは、失敗することが下手です。 失敗すれば凹むし、めげるし、ふさぎます。 不必要なほどダメージを受けます。 ダメージを受けるというよりも、自分自身にダメージを与えているというのが正解かもしれません。

失敗できる条件がプロジェクトに用意されているとしても、自分の気持ちとして失敗をゆるせるかはまた別の話。

感情は、本人以外が直接働きかけることはできません。 ですがそれでも、関わり方を工夫することはできるかも知れません。

以前、ソフトウェア開発を委託する立場だった時期がありました。 そのとき心がけていたこととして、報告される失敗に対して深刻な印象を与えないようにする、というものがあります。

深刻になり失敗してしまったことに心がとらわれていると、課題の解決のための力が削がれてしまいます。 当時はそこまで意識していたわけではないのですが、解決のために深刻にならないように、しかし真剣に真摯に取り組めるように、感情の余計な負担を減らしたかったのだと思います。

失敗したらどうするか?

共有しましょう。

なぜそうなっているのか。 その背景を知らないでいると、不用意に「改善」してしまい、失敗によって得られていた知見を失うという誤りをおかす危険があります。 その結果、その知見によって防がれていた課題が再び現れ、重大な事故につながることもあります。

失敗することが、うまくいかないことを知るための行為であるなら、その知識を活かさない手はありません。

ここでもまた「失敗を共有したらあげつらわれる」ことがあると、心理的な負担や障壁になってしまいます。 そうならないためにも、心理的に失敗できる環境が重要なのだと思います。

失敗のために

高い頂には広い裾野が必要です。 そして失敗は、成功のための裾野だと思います。

失敗だけが裾野ではありませんが、失敗もまた裾野を広げる役に立っているように思います。

過ちをおかすのではなく、裾野を広げるために、積極的に失敗することができたらと思います。

Rails / OSS パッチ会オンライン 2021年4月のお知らせ

2021年4月の Rails / OSS パッチ会を 4月22日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Zoom あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントが必要になると思います。

以下、前回の活動が関わる成果です。

koic: activerecord-oracle_enhanced-adapter

github.com

Oracle enhanced adapter の CI が master ブランチで落ちていたところ、私の手元で進めていた部分までを push して、続きを yahonda さんが kamipo さんとディスカッションしつつ、問題の解決まで進めてくれました。

osyo-manga: RuboCop

github.com

RuboCop でマルチバイト圏のソースコードを扱う場合の警告へのハイライト範囲の修正をされているパッチで、osyo-manga さんがパッチ会に問題点を持ってきてくれ、後日 PR を開いてくれました。このパッチへのエピソードについて以下の記事にも記しています。

koic.hatenablog.com


開催中の RailsConf や、最近の Ruby / Rails まわりの動向に関する話題などあるかもしれません。

その他の開催方針については以下の Gist に記していますので、ご参照ください。

Reboot Rails/OSS meetup online · GitHub

開発環境と金の弾丸

昨今の IT エンジニア募集企業では『開発 PC を選べる』というのが一つの潮流にある気がしています。永和システムマネジメント (以下 ESM) でも自分のパフォーマンスが最も出せる PC を選ぶといった制度が古くからあり、今回はそれにまつわるストーリーを書いてみます。

  • Apple の法人購入するときには、キーボードの選択に気をつけよう
  • 法人契約で間違えて購入したとき
  • 16コア、64GBは正義。コアとメモリは積めるだけ積め

以上のショートショート3本立てです。

Apple の法人購入で気をつけること

ESM では定期的に買い替えのタイミングを設けており、これはそんな MacBook Pro を買い換えるときのとある日の出来事です。

ESM で MacBook Pro を購入する際には Apple で法人契約購入するのですが、先日弊社メンバーで立て続けに起きた、ちょっとした注意点を共有しておきます。

舞台は以下のツイートです。

数日後、同僚にもまさかの同じことが起きました。

言い訳じみた原因としては、法人での購入画面への注意力散漫といったものだったと思われます。

MacBook Pro の構成をカスタマイズするときに、デフォルトで『日本語 (JIS) 』キーボードが選択されていたので、US 配列が良い場合は『英語 (米国) 』を選択し直す必要があります。

(注: 以下は US 配列キーボード「選択後」の画面です)

f:id:koic:20210402095901p:plain

CPU やメモリのカスタマイズと異なり、購入金額が変わるわけではないので、会社の予算に気を取られて見落とさないよう気をつけると良いです。

Apple の法人購入で間違えて購入した場合

一定期間の間であれば、交換してもらえます。私も諦めかけていたところ、バックオフィスのメンバーから交換の流れを教えてもらって US キーボードへの転身をとげました。 とりわけ Apple 社に迷惑を掛けてしまったあたり反省しかないのですが、おかげで満足なキーボードで生活できています。

あきらめる前に、お勤めの会社で交換可能か確認をとってみましょう。

16コア、64GBは正義。コアとメモリは積めるだけ積め

「買い替えてどうなったの?」というところで、OSS での開発を例に挙げると、RuboCop のフルビルドにかかる所要時間がおおよそ半分程度になりました。

RuboCop では test-queue を使ったテストの並列化をしているため、コア/ハイパースレッディングとメモリが増えればそれだけビルドの待ち時間が減るという公式です。

github.com

アクティビティモニタでみるとこんな感じで、すべてのコアが働いていることが確認できます。

f:id:koic:20210402095836p:plain

PC スペックとフルビルドの Before / After を並べてみます。

⏳ Before (1:54.53)

  • プロセッサ: 2.7 GHz クアッドコアIntel Core i7
  • メモリ: 16 GB 2133 MHz LPDDR3
% time bundle exec rake
Files:         567
Modules:        88 (   13 undocumented)
Classes:       529 (    2 undocumented)
Constants:     882 (  869 undocumented)
Attributes:     31 (    0 undocumented)
Methods:       990 (  867 undocumented)
 30.52% documented
Starting test-queue master (/tmp/test_queue_1738_17060.sock)

==> Summary (8 workers in 42.8440s)

    [ 1]                         1960 examples, 0 failures, 4 pending        80 suites in 24.9043s      (pid 1774 exit 0 )
    [ 2]                         2626 examples, 0 failures, 6 pending        87 suites in 24.9056s      (pid 1775 exit 0 )
    [ 3]                                     117 examples, 0 failures         1 suites in 42.8359s      (pid 1776 exit 0 )
    [ 4]                                    2309 examples, 0 failures        81 suites in 42.8358s      (pid 1777 exit 0 )
    [ 5]                                    2996 examples, 0 failures        87 suites in 42.8372s      (pid 1778 exit 0 )
    [ 6]                         1436 examples, 0 failures, 1 pending        78 suites in 42.8379s      (pid 1779 exit 0 )
    [ 7]                                     637 examples, 0 failures        53 suites in 42.8379s      (pid 1780 exit 0 )
    [ 8]                                    3490 examples, 0 failures        85 suites in 42.8312s      (pid 1781 exit 0 )

Starting test-queue master (/tmp/test_queue_1738_18680.sock)

==> Summary (8 workers in 41.1013s)

    [ 1]                                     117 examples, 0 failures         1 suites in 41.0932s      (pid 1860 exit 0 )
    [ 2]                                    1518 examples, 0 failures        78 suites in 41.0933s      (pid 1861 exit 0 )
    [ 3]                                    2480 examples, 0 failures        86 suites in 41.0947s      (pid 1862 exit 0 )
    [ 4]                                    2259 examples, 0 failures        86 suites in 41.0952s      (pid 1863 exit 0 )
    [ 5]                         2786 examples, 0 failures, 5 pending        87 suites in 41.0958s      (pid 1864 exit 0 )
    [ 6]                                    2066 examples, 0 failures        79 suites in 41.0960s      (pid 1865 exit 0 )
    [ 7]                                     591 examples, 0 failures        46 suites in 41.0957s      (pid 1866 exit 0 )
    [ 8]                         3754 examples, 0 failures, 6 pending        89 suites in 41.0892s      (pid 1867 exit 0 )

Running RuboCop...
Inspecting 1261 files
.............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

1261 files inspected, no offenses detected
bundle exec rake  515.00s user 34.02s system 479% cpu 1:54.53 total

⏳ After (1:11.55)

  • プロセッサ: 2.4 GHz 8コアIntel Core i9
  • メモリ: 64 GB 2667 MHz DDR4
% time bundle exec rake
Files:         567
Modules:        88 (   13 undocumented)
Classes:       529 (    2 undocumented)
Constants:     882 (  869 undocumented)
Attributes:     31 (    0 undocumented)
Methods:       990 (  867 undocumented)
 30.52% documented
Starting test-queue master (/tmp/test_queue_27867_17060.sock)

==> Summary (16 workers in 26.9534s)

    [ 1]                                     111 examples, 0 failures         1 suites in 12.8340s      (pid 27900 exit 0 )
    [ 2]                                    1527 examples, 0 failures        43 suites in 12.8344s      (pid 27901 exit 0 )
    [ 3]                                     117 examples, 0 failures         1 suites in 26.9430s      (pid 27902 exit 0 )
    [ 4]                         1029 examples, 0 failures, 4 pending        47 suites in 26.9427s      (pid 27903 exit 0 )
    [ 5]                                    1529 examples, 0 failures        51 suites in 26.9428s      (pid 27904 exit 0 )
    [ 6]                                    1699 examples, 0 failures        48 suites in 26.9439s      (pid 27905 exit 0 )
    [ 7]                                     959 examples, 0 failures        46 suites in 26.9438s      (pid 27906 exit 0 )
    [ 8]                                      68 examples, 0 failures         1 suites in 26.9437s      (pid 27907 exit 0 )
    [ 9]                                    1470 examples, 0 failures        50 suites in 26.9435s      (pid 27908 exit 0 )
    [10]                                     269 examples, 0 failures        21 suites in 26.9434s      (pid 27909 exit 0 )
    [11]                                      34 examples, 0 failures         1 suites in 26.9432s      (pid 27910 exit 0 )
    [12]                                     920 examples, 0 failures        45 suites in 26.9430s      (pid 27911 exit 0 )
    [13]                                    1473 examples, 0 failures        49 suites in 26.9428s      (pid 27912 exit 0 )
    [14]                         1171 examples, 0 failures, 1 pending        50 suites in 26.9431s      (pid 27913 exit 0 )
    [15]                         1630 examples, 0 failures, 1 pending        49 suites in 26.9428s      (pid 27914 exit 0 )
    [16]                         1565 examples, 0 failures, 5 pending        49 suites in 26.9414s      (pid 27915 exit 0 )

Starting test-queue master (/tmp/test_queue_27867_18680.sock)


==> Summary (16 workers in 28.2100s)

    [ 1]                                     111 examples, 0 failures         1 suites in 13.5583s      (pid 27954 exit 0 )
    [ 2]                                    1590 examples, 0 failures        48 suites in 13.5583s      (pid 27955 exit 0 )
    [ 3]                                    1316 examples, 0 failures        46 suites in 13.5583s      (pid 27956 exit 0 )
    [ 4]                         1231 examples, 0 failures, 5 pending        50 suites in 13.5582s      (pid 27957 exit 0 )
    [ 5]                                      68 examples, 0 failures         1 suites in 13.5581s      (pid 27958 exit 0 )
    [ 6]                                     256 examples, 0 failures        21 suites in 13.5578s      (pid 27959 exit 0 )
    [ 7]                                     895 examples, 0 failures        47 suites in 13.5578s      (pid 27960 exit 0 )
    [ 8]                                     931 examples, 0 failures        44 suites in 13.5579s      (pid 27961 exit 0 )
    [ 9]                                    1565 examples, 0 failures        48 suites in 13.5578s      (pid 27962 exit 0 )
    [10]                         1595 examples, 0 failures, 4 pending        45 suites in 13.5578s      (pid 27963 exit 0 )
    [11]                                     117 examples, 0 failures         1 suites in 28.2005s      (pid 27964 exit 0 )
    [12]                                      34 examples, 0 failures         1 suites in 28.2004s      (pid 27965 exit 0 )
    [13]                         1153 examples, 0 failures, 1 pending        49 suites in 28.2002s      (pid 27966 exit 0 )
    [14]                                    1596 examples, 0 failures        50 suites in 28.2003s      (pid 27967 exit 0 )
    [15]                                    1613 examples, 0 failures        50 suites in 28.2000s      (pid 27968 exit 0 )
    [16]                         1500 examples, 0 failures, 1 pending        50 suites in 28.2001s      (pid 27969 exit 0 )

Running RuboCop...
Inspecting 1261 files
.............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

1261 files inspected, no offenses detected
bundle exec rake  447.86s user 30.88s system 669% cpu 1:11.55 total

16 コア、64GB の開発環境は正義。金の弾丸が効いたことがわかります。

余談ですが、会社としては (業務がまわれば) Linux や WSL という選択肢もある中、私が MacBook を選んでいる理由は、Rails アプリケーション開発が仕事のベースにあるため、Rails の作者である DHH が選択している MacBook を使うことにしています。

これは『開発 PC を選べる』の根底にある「自分のパフォーマンスが最も出せる PC を選ぶ」への観点として、開発環境の構築にハマらないように、もしハマった時は The Rails Doctrine にある「数は安全性を生む」による解決が働きやすいことへの期待があるためです。

qiita.com

以上、金の弾丸によって開発効率を上げた一例の紹介でした。

もっとコアがあれば開発効率の向上に活かせるのに予算が足りないよ、、、ということであれば組織の予算をハックすることを検討できるかもしれません。PC の必要経費は年々上がっているので、時代背景とあっているか上長と掛け合ってみましょう。人件費に比べれば PC は安いもので、何より開発者にとって快適な開発環境はすこぶる良いものです (健闘を祈る!) 。

最後にいつものです。

agile.esm.co.jp

読みやすいテストコードについて考える

こんにちは、@kasumi8pon です。

アプリケーションの機能の追加開発をするときには、既存のテストに追加や修正を行う必要があります。また、既存のテストは現状の仕様を理解するのにも役に立ちます。そのため、テストコードが読みやすいと追加の開発がとても進めやすいです。ところが、わたしが開発しているアプリケーションのテストコードには、理解しやすく拡張もしやすいコードと、そうではないと感じるコードの両方が存在しました。よい機会なので、両者を比較しながら読みやすいテストコードについて考えてみたいと思います。なお、サンプルコードは RSpec と FactoryBot を利用して記載します。

内容を表す適切なラベルがついているか

メソッドの戻り値をそのままテストしているなど、テストの内容が自明である場合、以下のように説明を省略しても理解は容易です。

describe '#published?' do
  subject { book.published? }
  let(:book) { build(:book, published: true) }

  it { expect(subject).to be true }
end

しかし、期待する値の根拠が自明でないテストを書く場合、説明を記述するとのちの理解の助けになります。

# Bad
describe '#price' do
  subject { book.price }
  let(:book) { build(:book, price: 2_000, bargain_price: 500)

  # なんのケースをテストしてるの?
  it { expect(subject).to eq 1_500 }
end

# Good
describe '#price' do
  subject { book.price }
  let(:book) { build(:book, price: 2_000) }

  it { expect(subject).to eq 2_000 }

  context 'bargain_price がある場合' do
     let(:book) { build(:book, price: 2_000, bargain_price: 500) }

     it 'price から bargain_price の値を引かれた価格となること' do
      expect(subject).to eq 1_500
    end
  end
end

テストに説明があることで、他の箇所を参照せずともどんなテストなのかがわかるようになります。 小さなことに感じますが、仕様がわからないときにはとても理解の役に立ちます。

Feature Spec や System Spec を書く場合は、シナリオが簡潔に記載されているとわかりやすいです。これらのテストは複数の操作から成り立つことが多いため、説明がない場合ひと目でテストの内容を理解することは難しいためです。 操作部分のコードを追ってなんのテストをしているかを調べることはできますが、説明があればすぐ直感的に理解することができます。

describe '本の購入について' do
  # Bad (最後まで読まないとどんなテストかわからない)
  scenario do
    # ...
    # いろいろな操作
    # ...
    expect(page).to have_content '購入しました'
  end

  # Good (最初に何をテストしているのかがわかる)
  scenario '新発売の本の中から本を選んで購入する' do
    # ...
    # いろいろな操作
    # ...
    expect(page).to have_content '購入しました'
  end
end

RSpec には --format オプション(--format option - Command line - RSpec Core - RSpec - Relish)があり、--format documentation と指定することでテスト結果を仕様書のような形で出力することができます。このとき説明が不足していると文章として意味をなさない形になってしまいます。 --format documentation を利用したときに意味が通るようにテストを書くことは、読みやすいテストにも繋がります。

# Bad
本の購入について
  is expected to have text "購入しました"

# Good
本の購入について
  新発売の本の中から本を選んで購入する

構造が適切か

同じレベルのテストが違う階層にまたがっていたりすると、テストの全体像が把握しづらかったり、テストを追加するときに困ることがあります。以下の例では編集が新規登録時の一部の機能のように読めてしまい混乱しますし、削除の機能を追加するときにテストをどこに追加しようか迷ってしまいます。

# Bad (編集は新規登録と何か関係があるのかな?)
describe '本を新規登録する' do
  # 新規登録に関するテスト
  describe '本を編集する' do
    # 編集に関するテスト
  end
end

# Good (独立した同じレベルの機能は、同じ階層に書こう)
describe '本を新規登録する' do
  # 新規登録に関するテスト
end
describe '本を編集する' do
  # 編集に関するテスト
end

この例は極端ですが、現実のプロジェクトでは機能が多くて、テストが本来あるべき階層から飛び出してしまっているものを見かけることがあります。 このようなときも、RSpec の --format documentation で結果を出力して文章として読みづらいものがあると、不適切な階層にテストを書いていることがわかります。

また、テストデータのセットアップも適切な箇所で行うべきです。 下の例では、一見データは 20 件しか作成していないはずなのに、テスト内では 21 件目のデータについて言及しています。よくよく上の方を見ていくと、このテスト内で作成しているデータとは別でデータが作成されているようでした。

# Bad
describe '本の一覧機能について' do
  before do
    create(:book)
  end
  
  describe 'xxx' do
    # 他のテスト
  end

  describe 'ページネーションについて' do
    before do
      create_list(:book, 20)
    end
    
    # 20 冊しかつくっていないはずなのに、21 冊目はどこから出てきたのだろう?
    scenerio '21 冊目の本は 2 ページ目に表示される' do
      # 2 ページ目の表示を確認するテスト
    end
  end
end

必要な箇所のみに絞ってテストデータを生成すれば、読みやすくなります。 また、あとでテストを追加するときに認識していないデータによって想定外の状況が引き起こされることを防げます。

# Good
describe '本の一覧機能について' do
  describe 'xxx' do
    before do
      create(:book)
    end
    # 他のテスト
  end

  describe 'ページネーションについて' do
    before do
      create_list(:book, 21)
    end

    scenerio '21件目の本は 2 ページ目に表示される' do
      # 2ページ目の表示を確認するテスト
    end
  end
end

おわりに

わたしが思う読みやすいテストの特徴の一部を紹介しました。あくまでわたしが感じたことなので、別の書き方が読みやすいという方もいらっしゃると思います。テストの書き方には唯一の正解があるわけではないので当然です。しかし、唯一の正解がなかったとしても、読みやすさを意識せずに書いたコードと意識して書いたコードでは後から読み返したときに大きな違いがあるとわたしは考えています。この記事がチームメンバーや未来の自分の開発のしやすさのためにテストコードの読みやすさについて考えるきっかけになれば嬉しいです。