たくろぐ!

仕事はエンジニア、心はアーティスト

【Qiita】伊藤淳一さんのRspecのエントリを初心者向けにまとめてみた

伊藤淳一さんとは

Rubyistには馴染みの深いRspecだけど、そのRspecの第一人者であるのが伊藤淳一さんだ。
日本生まれのRubyだがそのテスティングフレームワークであるRspecの専門書を日本語で書いている人である(翻訳)。
Rspecの本を一度でも探したことがあるならどれほど有名な人かはわかるのだが、とにかく彼の書いた「Everyday Rails - RSpecによるRailsテスト入門」が人気でこれだけでも彼の残した功績は大きい(らしい)。

Rspecとは

Rspecというのは、Rubyの開発現場でよく使われるテスティングフレームワークだ。
テスティングフレームワークというのは、テストをするためのフレームワークと言う意味で、テストと言ってもここでは単体テスト(Unit Test(UT))のことを指している。

単体テストとは、誤解を恐れず言えばモジュール(つまり1機能を有したファイル群のこと)単位でソースコードのロジックが要件通りに動くかどうかを確認するテストのことだが、これを自動化させられるのがRspecというわけだ。

ちなみに同じ単体テスト用のフレームワークとして、Minitestというのがある。
これは現在Rubyの標準で付属されているフレームワークで、記述もRspecのようにDSLドメイン固有言語)でなくRubyで書かれている。
つまりRubyが書ければテストも同時に書くことができるという意味で学習コストが低い。

qiita.com

さらにRspecの特徴として、ビヘイビア駆動開発(behavior driven development(BDD))というものが挙げられる。
ざっくり言えば、メソッドなどの振る舞い(=Behaviour)に焦点を当ててテストをする方法だ。
RspecはこのBDDに特化したテスティングフレームワークなのである。

テスト全般に関しては以下のQiitaのエントリがわかりやすい。

qiita.com

ちなみにRspec単体テスト用のテストツールということだが、結合テスト以降ももちろんテストのフレームワークはある。
結合テストはエンドツーエンド(End-to-End)テストとも言われ、こっちの末端からあっちの末端までというモジュール間を繋いだ、実際の動作に近いテストのことだ。
Rubyではひと昔前はCucumberというフレームワークだったようだが、現在はTurnipというものが主流らしい。
ちなみにブラウザテスト(統合テストに近いかな)ではSeleniumというフレームワークが有名だ。

結構古いがこれわかりやすい[中級者向け]

エンドツーエンドテストの自動化は Cucumber から Turnip へ

プログラミング初心者を悩ますテストの違い[初心者向け]

[Testing] システムテストとエンドツーエンドテストの違い terminology qa | CODE Q&A [日本語]

itpgsepm-note.com

早速やってみる

では早速だが、Rspecを書いてみる。
特にプログラミングはそうだが、習うより慣れろだ。
実際に手を動かしてみて初めてわかることがプログラミングにはある。

最初のRspec

まずは簡単なテストを作成してみる。

知っている人には違和感のあるテストかもしれないが、覚えることを主眼に簡潔なテストとした。

describe '四則演算' do
  it '1 + 1 は 2 になること' do
    (1 + 1).should eq 2
  end
  it '1 - 1 は 0 になること' do
    (1 - 1).should eq 2
  end
  it '1 * 1 は 1 になること' do
    (1 * 1).should eq 2
  end
  it '1 / 1 は 1 になること' do
    (1 / 1).should eq 2
  end
end

参照元qiita.com

describeはテストをまとめる部分。
四則演算ということなら例のように足し算だけでなく、引き算、割り算、掛け算もこのdescribeのブロックの中に入る。

これがdescribeのひとまとまりとなる。

次にitだが、これはエクスペクテーションを作るおまじないだ。

エクスペクテーションとはテストの単位のことで、この単位にしたがってテストを実行し、その結果がコンソールに出力されていく。
コンソールっていうのはこんなやつ。

4 examples, 0 failures, 4 passed

Finished in *.******* seconds

Process finished with exit code 0

上記の例では4つのテストが実行された結果、その全てがpassed(通った)ということがわかる。
四則演算のテストをしているから4つのテストがあるんだな。

続いてshouldの部分だが、実はこれは古い構文で、(〜であるべき)という意味を持つ。
つまり、「1 + 1は2であるべきだ」という意味になる。

現在はshouldの代わりにexpect toという構文を使うが、どちらもeqやincludeなどのマッチャと呼ばれる部品を後ろに取る。
expect toの意味は(〜ということが期待されている)という意味で、「it(それが) expect to(期待されている) (be) equal (to) (等位であると)」と言う意味になる。
shouldとも少し書き方が異なる。

qiita.com

上記の四則演算をexpect toで書くと以下のようになる。

expect(1 + 1).to eq 2

少し難しかったかな。

expect toまで覚えておけば、ビギナーレベルはクリアだ。

次にもう少し語彙力を高めて、見やすいテストコードを作成していこう。

ビギナーを超えるためのRspec

さて次にさらに可読性を意識した構文を学んでいくぞ。

describe 'テストする対象(例:Stack)' do
  context 'テストの状況(例:新しく生成したとき)' do
    #テストを実行する前処理
    before do
      @stack = Stack.new
    end
 
    it 'テストの説明(例:スタックが空であること)' do
      #期待する出力との照合
      @stack.should be_empty
    end
  end
end

参照元www.ruby.or.jp

さて、新しい構文が出てきたな。

まずcontextだが、これはdescribeでまとまりを作ったら条件にしたがって分割していくというイメージだ。

例えば例のスタックという箱を新しく作った場合のコンテキスト(=文脈と言う意味)、
そのスタックに値を追加する場合のコンテキスト、
値を取り出す場合のコンテキストみたいな感じだな。

https://wa3.i-3-i.info/word14717.html

次のbeforeでは前処理を記載する。
そのコンテキストがパス(通る)するためにテストデータを操作しておくための構文だ。
ちなみに後処理の場合はafterという構文を使用する。

ここまで理解できれば、基礎はもう十分だ。

あとは都度調べながらテストコードを書くことができるようになっているはずだ。

これが書ければ中級のRspec

次にリファクタリングをするための構文を覚える。

今までの知識があれば、以下のテストコードの意味がわかるだろう。

describe User do
  describe '#greet' do
    before do
      @params = { name: 'たろう' }
    end
    context '12歳以下の場合' do
      before do
        @params.merge!(age: 12)
      end
      it 'ひらがなで答えること' do
        user = User.new(@params)
        expect(user.greet).to eq 'ぼくはたろうだよ。'
      end
    end
    context '13歳以上の場合' do
      before do
        @params.merge!(age: 13)
      end
      it '漢字で答えること' do
        user = User.new(@params)
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

引用元:qiita.com

これをletを使用すると以下のようになる。

describe User do
  describe '#greet' do
    let(:params) { { name: 'たろう' } }
    context '12歳以下の場合' do
      before do
        params.merge!(age: 12)
      end
      it 'ひらがなで答えること' do
        user = User.new(params)
        expect(user.greet).to eq 'ぼくはたろうだよ。'
      end
    end
    context '13歳以上の場合' do
      before do
        params.merge!(age: 13)
      end
      it '漢字で答えること' do
        user = User.new(params)
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

letは簡単に言えば、事前に変数宣言するための構文だ。
つまり、paramsという変数にブロックで{name: 'たろう'}という、ハッシュという配列に似たデータ構造を代入している。

beforeの部分をletという構文で書くと、インスタンス変数を書かなくてもよくなっている。

さらにこのテストコードをリファクタリングしてみる。

describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) { { name: 'たろう', age: age } }
    context '12歳以下の場合' do
      let(:age) { 12 }
      it 'ひらがなで答えること' do
        expect(user.greet).to eq 'ぼくはたろうだよ。'
      end
    end
    context '13歳以上の場合' do
      let(:age) { 13 }
      it '漢字で答えること' do
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

先ほどハッシュにname: 'たろう'を配列の要素として持たせたわけだが、それにさらにage: ageを追加していることがわかる。

letを使うことでテストコードがかなりコンパクトになったことがわかっただろうか。

実はパフォーマンスにおいてもletのメリットはあるのだが、ここでは割愛することにする(詳しく知りたければ参照元のエントリ「遅延評価」の項目を参照。)。

これでついに上級Rspec

最後によりDRY(同じ部分を排除して、冗長な記述を避けること)にするための構文を覚えていく。

describe User do
  describe '#greet' do
    let(:user) { User.new(name: 'たろう', age: age) }
    subject { user.greet }

    shared_examples '子どものあいさつ' do
      it { is_expected.to eq 'ぼくはたろうだよ。' }
    end
    context '0歳の場合' do
      let(:age) { 0 }
      it_behaves_like '子どものあいさつ'
    end
    context '12歳の場合' do
      let(:age) { 12 }
      it_behaves_like '子どものあいさつ'
    end

    shared_examples '大人のあいさつ' do
      it { is_expected.to eq '僕はたろうです。' }
    end
    context '13歳の場合' do
      let(:age) { 13 }
      it_behaves_like '大人のあいさつ'
    end
    context '100歳の場合' do
      let(:age) { 100 }
      it_behaves_like '大人のあいさつ'
    end
  end
end

ここではshared_examplesという構文を使ってエクスペクテーションを使い回ししている。

つまりshared_examples(共有されたexample(というテスト単位))で定義したものをit_behaves_like(〜のように振る舞う)の引数として呼び出すことでDRYなテストコードが書ける。

同じように、contextでも使い回しができる。

shared_contextで定義したものをinclude_contextで呼び出すことでDRYが実現できる。

describe User do
  describe '#greet' do
    let(:user) { User.new(name: 'たろう', age: age) }
    subject { user.greet }
    context '12歳以下の場合' do
      let(:age) { 12 }
      it { is_expected.to eq 'ぼくはたろうだよ。' }
    end
    context '13歳以上の場合' do
      let(:age) { 13 }
      it { is_expected.to eq '僕はたろうです。' }
    end
  end

  describe '#child?' do
    let(:user) { User.new(name: 'たろう', age: age) }
    subject { user.child? }
    context '12歳以下の場合' do
      let(:age) { 12 }
      it { is_expected.to eq true }
    end
    context '13歳以上の場合' do
      let(:age) { 13 }
      it { is_expected.to eq false }
    end
  end
end

書いてわかること

テストを書くメリットは以下の通りだ。

用語的な何か

  • example
    テストの最小単位のこと

  • describe
    テスト対象を指す。
    複数のテストをまとめて、その中で対象を分けるときはネストする。

  • context
    条件を指定するときなどに使う、ON/OFFやTrue/falseのときなど分岐に使う。
    describeのエイリアス
    describeが対象を指すなら、contextは状況を指す。

  • before
    そのテストをする前の処理として記述する。

  • after
    beforeの逆。後に何かの処理をさせるときに記述する。

  • it
    実際のテストを記載するときに使う。
    マッチャなどと一緒に使うことが多い。

  • should
    逆はshould_not

  • expect_to

  • subject
    レシーバを指定するときに使う。

  • its

  • shared_examples

  • let
    ブロックの評価を引数のSymbolとして利用できる。

  • double
    スタブを作るときに使用する

  • stub
    doubleで作成したスタブにメソッドを追加できる。 もちろんオブジェクトやクラスにも使える。

  • say
    モック作成のときに使う。

  • (should_)receive
    sayでメソッドが呼ばれたら、ここで定義した値を返す。

参考にしたエントリ

qiita.com

qiita.com

www.ruby.or.jp

スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)