「Ruby on Rails6 実践ガイド」勉強メモ(No.2)
「Ruby on Rails 6 実践ガイド」のひとり読書会の2回目です。
今回の内容はChapter4のRSpecです。RSpecを使うのははじめて。
RSpceとは
テストフレームワーク。Ruby on RailsにはMinitestというテストフレームワークが標準で組み込まれている。
RSpceはMinitestにはない優れた機能があるが、書き方が独特で慣れないという声もある。
RSpecの初期設定
RailsでRSpecを利用する場合、次のコマンドを実行する必要がある。
% 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からきてるのか〜。ビヘイビア駆動開発もはじめて知って学びが多かった。