Hit the books!!

プログラミング学習記録

「Ruby on Rails6 実践ガイド」勉強メモ(No.2)

Ruby on Rails 6 実践ガイド」のひとり読書会の2回目です。

ud-ike.hatenablog.com

今回の内容はChapter4のRSpecです。RSpecを使うのははじめて。

RSpceとは

テストフレームワークRuby on RailsにはMinitestというテストフレームワークが標準で組み込まれている。

RSpceはMinitestにはない優れた機能があるが、書き方が独特で慣れないという声もある。

RSpecの初期設定

RailsRSpecを利用する場合、次のコマンドを実行する必要がある。

% bin/rails g rspec:install

ターミナルで以下のような結果が表示され、ファイルが生成される。

Running via Spring preloader in process 6754
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

簡単なテストコード例

まずは実際に簡単なテストコードを書いて実行してみよう。

Baukis2直下にあるspecディレクトリの下にexperimentsというサブディレクトリを作り、次の内容でstring_spec.rbというファイルを作成する。

# spec/experiments/string_spec.rb 

require 'spec_helper'

describe String do
  describe '#<<' do
    example "文字の追加" do
      s = 'ABC'
      s << 'D'
      expect(s.size).to eq(4)
    end
  end
end

RSpecのテストコードは通常specディレクトリの配下に置き、ファイル名の末尾は'_spec.rb'とする。(specファイルと呼ぶ)

specディレクトリのサブディレクトリには分類してspecファイルを配置する。例えば、モデルクラスに関するspecファイルはspec/modelsディレクトリに、APIに関するものはspec/requestsディレクトリに置くことが慣習的に決まっている。

独自のサブディレクトリを用意しても問題ないため、ここではexperimentsサブディレクトリを作成した。

テストコードの本体は次の3行。

      s = 'ABC'
      s << 'D'
      expect(s.size).to eq(4)

変数sに'ABC'という文字列をセットしたあと、'D'という文字を追加している。expectメソッドで変数sの状態(s.size)を調べ、長さが4であるかをチェックしている。

このspecファイルを実行してみよう。ターミナルで以下のコマンドを実行する。

 % rspec spec/experiments/string_spec.rb

すると、次のような結果が表示される。

.

Finished in 0.0053 seconds (files took 0.13808 seconds to load)
1 example, 0 failures

rspecコマンドはパスを指定して個別に実行することも、ディレクトリを指定して一括で実行することも可能である。

% rspec specもしくは$ rspecとしても同じ結果になる。

example

exampleとは

RSpecはビヘイビア駆動開発(Behavior Driven Development:BDD)というプログラム開発手法をRubyで実践するために作られたテストフレームワークである。

RSpecは独特の用語を採用しており、そのひとつがエグザンプル(example)である。

先ほどのstring_spec.rbの例では、

    example "文字の追加" do
      s = 'ABC'
      s << 'D'
      expect(s.size).to eq(4)
    end

この部分がexampleである。

exampleメソッドの引数には、短い説明文を指定する。exampleメソッドにはitという別名があり、これを利用すると次のように書くことができる。

    it 'appends a character' do
      s = 'ABC'
      s << 'D'
      expect(s.size).to eq(4)
    end

説明文を英語で記述する場合、itと引数で1つの文のように見えるためしばしば別名が使われる。日本語での記述にもitを使用しても問題ないが、あまり使わないみたい。

さらにspecifyという別名もある。

ビヘイビア駆動開発では、テストコードによってソフトウェアの仕様(specification)が定義される。RSpecでは単にソフトウェアのテスツをするだけでなく、仕様を記述することに重点を置いている。

example group

関連するいくつかのエグザンプルをエグザンプルグループ(example group)としてまとめることができる。

先ほどの例で言うと、describeとendで囲まれた部分がexample groupである。

# spec/experiments/string_spec.rb 

require 'spec_helper'

describe String do
  describe '#<<' do
    example "文字の追加" do
      (省略)
    end
  end
end

describeメソッドの引数には、クラスまたは文字列を指定する。

example groupは入れ子構造にすることが可能で、ここでは全体としてStringクラスに関する仕様をまとめたexample groupを作ったあと、<<メソッドに関する仕様をまとめるためのexample groupを作っている。 <<メソッドの働きの一例が、文字の追加というexampleとして表現されている。

<<の前の#は、インスタンスメソッドであることを示す慣用的な記号で、RSpecにとっては特別な意味はない。

テスト結果の読み方

エラーの読み方

テストが失敗するとどんな結果になるのかみてみよう。先ほどの例にexampleを追記する。

# spec/experiments/string_spec.rb 

require 'spec_helper'

describe String do
  describe '#<<' do
    example "文字の追加" do
      (省略)
    end

    example "nilの追加" do
      s = 'ABC'
      s << nil
      expect(s.size).to eq(4)
    end
  end
end

実行すると、以下のように表示される。

 % rspec spec/experiments/string_spec.rb
.F

Failures:

  1) String#<< nilの追加
     Failure/Error: s << nil
     
     TypeError:
       no implicit conversion of nil into String
     # ./spec/experiments/string_spec.rb:13:in `block (3 levels) in <top (required)>'

Finished in 0.00407 seconds (files took 0.11564 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/experiments/string_spec.rb:11 # String#<< nilの追加

1行目のドット(.)とFの数はexample数と同じで、.は成功、Fは失敗を表す。順番はランダムなので入れ替わる可能性がある。

Failures:以降は失敗したexampleについて説明が表示されている。TypeErrorが発生していることがわかり、#ではじまる行にはエラーの発生箇所が書かれている。

pendingメソッド

テストが失敗しても、原因がわからなかったりという理由ですぐに修正できないことがある。その場合は、pendingメソッドを使ってexampleに保留中という印をつける。

# spec/experiments/string_spec.rb 

require 'spec_helper'

describe String do
  describe '#<<' do
    example "文字の追加" do
      (省略)
    end

    example "nilの追加" do
      pending("調査中") # 追記
      s = 'ABC'
      s << nil
      expect(s.size).to eq(4)
    end
  end
end

実行すると、1行目のFだったものが*に変わる。

% rspec spec/experiments/string_spec.rb
.*

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) String#<< nilの追加
     # 調査中
     Failure/Error: s << nil
     
     TypeError:
       no implicit conversion of nil into String
     # ./spec/experiments/string_spec.rb:14:in `block (3 levels) in <top (required)>'

Finished in 0.00392 seconds (files took 0.11606 seconds to load)
2 examples, 0 failures, 1 pending

xexampleメソッド

pendingメソッドを書くのが面倒な場合には、exampleをxexampleに書き換える方法もある。

# spec/experiments/string_spec.rb 

require 'spec_helper'

describe String do
  describe '#<<' do
    example "文字の追加" do
      (省略)
    end

    xexample "nilの追加" do # 変更
      s = 'ABC'
      s << nil
      expect(s.size).to eq(4)
    end
  end
end

実行結果は以下のようになる。

% rspec spec/experiments/string_spec.rb
.*

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) String#<< nilの追加
     # Temporarily skipped with xexample
     # ./spec/experiments/string_spec.rb:11


Finished in 0.00384 seconds (files took 0.1155 seconds to load)
2 examples, 0 failures, 1 pending

expectメソッドとマッチャー

例ではexpect(s.size).to eq(4)という形で使用されているexpectメソッドについて。

オブジェクトを対象にする場合

一般的な使用法は次の通り。

expect(T).to M

Tをターゲット、Mをマッチャーと呼ぶ。

expect(s.size).to eq(4)

の場合、ターゲットはs.sizeメソッドが返すオブジェクト、マッチャーはeq(4)が返すオブジェクトである。

マッチャーとは、ターゲットに指定されたオブジェクトがある条件を満たすかどうかを調べるオブジェクトである。ターゲットがその条件を満たさなければ失敗となる。

eqメソッドは、引数に指定したオブジェクトとターゲットが等しいかどうかを調べるマッチャーを返す。

expectのあとのtoをnot_toに変えると全体の意味が反転する。

RSpecにはたくさんのマッチャーが用意されている。

ブロックを対象にする場合

expectメソッドにはブロックを対象にする使用法もある。

expect { ... }.to M

例は以下の通り。

# spec/experiments/string_spec.rb 

require 'spec_helper'

(省略)

    example "nilは追加できない" do
      s = 'ABC'
      expect { s << nil }.to raise_error(TypeError) # 例外TypeErrorが発生すれば成功
    end
  end
end

TypeErrorが発生するので成功となる。

exampleの絞り込み

行番号による絞り込み

パスのうしろに行番号を指定すれば、specファイルの中の特定のexampleだけを実行することができる。

% rspec spec/experiments/string_spec.rb:11

タグによる絞り込み

次のようにexampleメソッドの第2引数に:exceptionというシンボルを追記すると、

# spec/experiments/string_spec.rb 

require 'spec_helper'

(省略)

    example "nilは追加できない", :exception do # 変更

(省略)

以下のように:exceptionタグのついたexampleだけをまとめて実行できる。

% rspec spec --tag=exception

感想

RSpectという名前は仕様のspecificationからきてるのか〜。ビヘイビア駆動開発もはじめて知って学びが多かった。