Help us understand the problem. What is going on with this article?

サヨナラBetter Specs!? 雑で気楽なRSpecのススメ

More than 1 year has passed since last update.

はじめに

RSpecって結構「RSpecらしく書くこと」を求められたりします。
たとえば、describeやcontextでしっかりグループを分けましょう、再利用するデータはletやsubjectに切り出しましょう、ひとつのexample(it)の中でテストするのはひとつの項目だけにしましょう、等々の方針です。

よくあるのが「Better Specsを読んで、こんなふうに書くようにしましょう!」っていうパターンですね。

Better Specs

しかし僕の場合、最近は「そこまでがんばってキレイにしなくてもいいのでは?」と考えるようになってきています。
その結果、テストコードがだんだん雑になってきています。

というわけで、この記事では最近僕が実践している「雑なRSpec」の書き方を紹介します。

備考

この記事は以前自分のブログに書いた内容を加筆・修正したものです。
「雑なRSpec」以外の話題についても書いているので、よかったらこちらもどうぞ。

テストコードにまつわる5つのエトセトラ - give IT a try

「RSpecらしく」書くとこんな感じ

百聞は一見にしかず、というわけで「RSpecらしく書いたRSpec」と「雑なRSpec」の具体例を挙げてみます。
まずは「RSpecらしく書いたRSpec」のコード例です。

describe Cloth do
  describe '#half_price' do
    let(:cloth) { Cloth.new('RSpec Tシャツ', price) }
    subject { cloth.half_price }
    context '割り切れる場合' do
      let(:price) { 1000 }
      it { is_expected.to eq 500 }
    end
    context '割り切れる場合・その2' do
      let(:price) { 2000 }
      it { is_expected.to eq 1000 }
    end
    context '端数が出る場合' do
      let(:price) { 999 }
      it { is_expected.to eq 499 }
    end
  end
end

RSpecらしく書いたコードは、

  • 条件をcontextに分けている
  • テスト内で使われる変数をletに切り出している
  • 遅延評価されるletの特性を活かして、context内で別々のpriceを設定している
  • テストするメソッドをsubjectに設定し、it { is_expected.to ... }の形で検証している
  • itの中でテストするのは1つだけ

みたいな感じです。
RSpecに詳しい人が見たら、「そうそう、RSpecで書くならこうだよね!」と思われるのではないでしょうか。

「雑に書く」とこんな感じ

一方、僕が最近よく書く「雑なRSpec」はこんなコードになります。

describe Cloth do
  describe '#half_price' do
    example do
      # 割り切れる場合
      cloth = Cloth.new('RSpec Tシャツ', 1000)
      expect(cloth.half_price).to eq 500

      # 割り切れる場合
      cloth.price = 2000
      expect(cloth.half_price).to eq 1000

      # 端数が出る場合
      cloth.price = 999
      expect(cloth.half_price).to eq 499
    end
  end
end

雑なRSpecのポイントは、

  • describeやcontextのグループ分けは必要最小限にする
  • contextに相当するものはコメントで書く
  • ひとつのexampleの中で、まとめて複数のテストを書いてしまう
  • letやsubjectに切り出さず、ローカル変数にしたり、毎回同じようなコードをベタ書きしたりする
  • it 'returns half price'it '半額の値を返す'のような説明文を省略して、exampleだけで済ませる

みたいな感じです。

雑なRSpecのメリット・デメリット

メリット・デメリットはいろいろありますが、まずメリットを挙げると、

  • 思いついたテストパターンを上から下へさくっと書ける
  • どれをletやsubjectにすべきか、どうcontextを分割すべきか、といった「テストコードの設計」に頭を使わなくて済む
  • 「意図した通りにコードが動いていることを検証できる」という点では、RSpecらしいテストコードと変わりがない

みたいになるかな、と思います。

デメリットとしては、

  • 1つのexample内で複数のテストを実行しているので、途中で失敗するとその先のテストが成功するか失敗するかわからない
  • コードの重複が多いので、仕様変更が入ると同じような修正を繰り返すことになるかもしれない
  • --format documentationのオプションを付けて実行したときの出力結果が、文字通り「雑」になる
  • 「こんなRSpecは許せない!RSpecらしく書き直せ!:anger:」と怒り出す人が出てくるかも?

みたいなところでしょうか。

「RSpecらしいRSpec」を推奨しすぎると初心者を遠ざける?

今回「雑なRSpec」の書き方を紹介した理由のひとつは、「RSpecが苦手」「RSpecがキライ」という人たちに、「いやいや、別にこういう書き方をしたっていいんだよ」という話をしたかったからです。

巷にあふれる「よいRSpecの書き方」が全部「RSpecらしいRSpec」になると、初心者の人たちが「RSpec怖い」「RSpec難しい」「RSpecなんて書きたくない」と思ってしまうのではないか?とちょっと危惧しています。

変にハードルを上げすぎて「こんなふうにRSpecを使いこなせないからテストを書きたくない」「RSpecらしく書くために何時間も格闘している」みたいなことが起きると、非常にもったいないです。
形は何であれ、まずは「テストを書く」という習慣を身につけてもらう方が大事だと思います。

実際こうやって比べてみると、「雑なRSpec」の方がまだ初心者の方にとってはわかりやすい(=理解すべきRSpecのDSLが少ない)はず、と考えているのですが、いかがでしょうか?

重要:「RSpecらしいRSpec」を完全に放棄したわけではない

とはいえ、僕も「RSpecらしいRSpec」を完全に放棄したわけではありません。
テストコードの内容によってはRSpecらしく書いた方が、読み書きがしやすかったり、保守性が高かったりするケースもあります。
そういう場合は「RSpecらしいRSpec」を書くようにしています。

ケースバイケースで「RSpecらしいRSpec」と「雑なRSpec」を使い分けている感じですね。

「よくわかんないけど、これが巷で言う守破離の"離"ってやつ?」なんて自分では考えたりしています。(間違ってたらすいません)

カスタムマッチャよりも検証用の野良メソッド

あと、少し話は変わりますが、最近はspec内で独自の検証メソッドを作ってそれを使うこともよくあります。
たとえばこんな感じです。

describe UsersController do
  # 検証用メソッド
  def assert_require_login
    expect(response).to redirect_to root_path
  end
  # 検証用メソッド
  def assert_ok
    expect(response).to have_http_status 200
  end
  describe '#index' do
    example do
      # ログインしていない場合
      get :index
      assert_require_login

      # ログインしている場合
      sign_in
      assert_ok
    end
  end
  describe '#show' do
    let(:user) { create :user } 
    example do
      # ログインしていない場合
      get :show, params: { id: user.id }
      assert_require_login

      # ログインしている場合
      get :show, params: { id: user.id }
      assert_ok
    end
  end
end

実際はここまで単純なテストだと検証メソッドを作らずにベタ書きすると思いますが、あくまで利用イメージとして見てやってください。

要するに「ややこしい検証が必要な箇所がいくつかある。でもそれを毎回ベタ書きするのは大変なので共通化したい。とはいえ、カスタムマッチャを用意するのは腰が重い」みたいなときに、テスト内でassert_xxxみたいな検証用メソッドを作ってそれを再利用する、というアプローチです。

assert_xxxというメソッド名は全然RSpecらしくない、むしろMinitestやtest-unitのようなxUnit系のメソッド名ですが、明示的で分かりやすいので僕はassert_xxxみたいなメソッド名を付けています。

カスタムマッチャを作らず、野良メソッド(その場限りの使い捨てメソッド)で済ませるというのがRSpecらしくないですし、「雑」ですね。

データのセットアップ等でも独自メソッドを使う

また、検証用のメソッドだけでなく、データのセットアップなどでもテスト内でメソッドを作ってそれを利用することがあります。
イメージ的には次のような感じです。

describe Blog do
  let(:alice) { create :user, name: 'Alice' }
  let(:bob) { create :user, name: 'Bob' }
  let(:chris) { create :user, name: 'Chris' }

  def create_blogs(user)
    # テスト用データをこしらえるためのややこしい処理
  end

  before do
    [alice, bob, chris].each do |user|
      # テスト内で作ったメソッドを呼び出す
      create_blogs(user)
    end
  end
  example do
    # 何かのテスト
  end
end

検証用メソッドもデータセットアップのメソッドも、複数のテストファイルで利用する場合はヘルパーマクロとしてSpec全体で使えるようにした方がいいですが、そうでなければファイル固有のメソッドとして定義して使えばいいと思います。

2017.01.12追記:eqマッチャだけでもいいじゃない

Minitestやtest-unitの長所としてよく耳にするのが、「assert_equalだけ覚えておけばいいからラク」というものです。
しかし、僕からすると「いや、RSpecだってeqマッチャだけ覚えておけば同じやん?」と思ってしまいます。

つまり、Minitestやtest-unitで

assert_equal B, A

と書けるテストは、RSpecで

expect(A).to eq B

と書き直すことができます。

もちろん、RSpecにはマッチャがたくさん用意されているので、それを使いこなせた方が良いですが、ちゃんと検証すべき内容が検証できるなら「全部eqマッチャだけで済ませる」という選択肢もありだと思います。

まとめ

というわけで、この記事では最近僕が実践している「雑なRSpec」の書き方を紹介してみました。

Better Specsに載っているような書き方は全く知らないよりも知っている方がいいですし、実際にそう書けるに越したことはありません。
ですが、このルールを「いつも絶対守るべき!!」と思い込んでしまうとテストを書く効率を下げてしまいます。

また、他の人に対して「RSpecらしいRSpec」を強く推奨しすぎると、初心者のハードルを無駄に上げてしまうかもしれません。

「ハンマーを持つ人にはすべてが釘に見える」ではありませんが、RSpecのDSLを覚えると「隅から隅まで徹底的にDSLを活用すべし」という気持ちが出てくるのは分かります。
実際、昔の僕がそんな感じだったので(苦笑)。

ですが、ある程度DSLを使いこなせるようになったら、状況に応じて「あえて雑に書く」のもひとつの手です。
ある意味、すべてRSpecらしく書くことよりも上級者向けのテクニックかもしれませんが、もしあなたの周り「RSpecらしさの追求」に時間をかけすぎている人がいたら、「もっと気楽に書いていいんやで:kissing_closed_eyes:」とこの記事を見せてやってください。

Special thanks

「雑なRSpec」というフレーズはこちらの記事を参考にさせてもらいました。

俺のRSpecがこんなに雑なわけがない - Qiita

もともとは「Minitestっぽい書き方」と自分の中で読んでいたのですが、「"雑なRSpec"って響きがいいな」と思い、僕も「雑なRSpec」と呼ぶようになりました。

上の記事は僕のアプローチと同じところもあれば、違うところもあります。
ですが、「Better Specs至上主義から離れて、もっと気楽に書こう」という基本方針は僕と同じですね。

あわせて読みたい

こちらは仕事で書いたRSpecを適当にフェイクしたものです。
そこそこ「雑」に書いている一例としてはわかりやすいかもしれません。

https://gist.github.com/JunichiIto/0427c51d75b064733d3829acd32a7777

そもそもRSpecを全く知らないので基礎の基礎から学びたい、という方はこちらの記事をご覧ください。

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita

もっと本格的にRSpecの書き方を学びたい方は 「Everyday Rails - RSpecによるRailsテスト入門」 をどうぞ!(宣伝)

Everyday Rails - RSpecによるRailsテスト入門
hero.png

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away