RSpecを使ってみよう
RSpecとは?
RSpecは、Rubyのコードをテストするために作られた、「Rubyで書かれたテストフレームワーク」です。
テスト駆動開発(TDD)の一種である振る舞い駆動開発(BDD)が行いやすい設計になっています。
ここでは、簡単な例を用いてRSpecを使った一連の開発の流れを紹介します。
セットアップ編
1. 作業用のディレクトリをつくる
まずは作業用のディレクトリ作成しましょう。
ここでは、ディレクトリ名を _rspec_tutorial_ とします。
1
2
3
$ cd ~/Documents/ruby_lecture/
$ mkdir rspec_tutorial
$ cd rspec_tutorial
2. RSpecを入れる
bundle init
コマンドを実行して、 Gemfile を生成します。
1
$ bundle init
Gemfile の中身は以下のようにします。
1
2
3
4
5
6
7
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem 'rspec', '~> 3.8' # <= ここを追加
bundle installで、RSpecをインストールします。
1
$ bundle install --path vendor/bundle
3. 初期設定
RSpecで使うファイルを用意します。
1
$ bundle exec rspec --init
この段階で、ディレクトリ構成はこのようになっていると思います。
1
2
3
4
5
6
7
8
{作業ディレクトリ}/rspec_tutorial/
├ .bundle
├ .rspec
├ Gemfile
├ Gemfile.lock
├ spec/
│ └ spec_helper.rb
└ vendor/
- .rspec : RSpec実行時に指定するオプションなどを記述するファイル
- spec/ : テストコードを置くディレクトリ
- spec_helper.rb : テストで利用するヘルパメソッドや設定などを記述。設定についてはこちらを参照。
ここでは、後のわかりやすさのため .rspec に以下を記述しましょう。
1
2
3
--require spec_helper
--format documentation
--color
--format documentation
: テスト内容をわかりやすくコンソールに表示--color
: コンソールの表示に色をつける
Note : RSpecのディレクトリ構成
RSpecを利用する場合、プロダクトコードとテストコードは以下のようなディレクトリ構成にするのが一般的です。
1
2
3
4
5
プロジェクト/
├ hoge.rb # プロダクトコード
│
└ spec/
└ hoge_spec.rb # hoge.rbのテストコード
テスト対象が lib
のようなディレクトリにある場合も同様です。
1
2
3
4
5
6
7
プロジェクト/
├ lib/
│ └ hoge.rb # プロダクトコード(モジュールなど)
│
└ spec/
└ lib/
└ hoge_spec.rb # hoge.rbのテストコード
ここまでできたら、せっかくなのでgitで作業記録を残しておきましょう。
.gitignore ファイルで .bundle や vendor/ を監視対象から外すことを忘れないようにしましょう。
1
2
$ git add .
$ git commit -m "first commit"
実践編
前提
次のメソッドをTDDで実装することを考えます。
1
2
1から100までの数をプリントするプログラムを書け。
ただし3の倍数の時は数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
- 方針
- 標準出力のテストは難しい
- 「一つの『自然数』を受け取って、その値に応じた『文字列』を返すメソッド
fizzbuzz(natural_number)
」と「1から100までの自然数をfizzbuzz
メソッドに渡して、結果をプリントするプログラム」に分離する。 fizzbuzz
メソッドはin/outが明確に定義できるためテストしやすい
- 「一つの『自然数』を受け取って、その値に応じた『文字列』を返すメソッド
- 標準出力のテストは難しい
最初の一歩
RSpecにおけるテストコードは、以下のようなフォーマットで書きます。
actual
は実際の値(評価値)、 expected
は期待する値です。
1
2
3
4
5
6
RSpec.describe 'テスト対象' do
context '条件' do
it '期待する結果' do
expect(actual).to eq(expected)
end
end
Step Up! : describeとcontext
RSpecのテストコードにおいて、 describe
と context
は「内部的には同じもの(エイリアス)」です。
したがって、先程の sample_spec1.rb を以下のように書き換えても動作的には問題ありません。
1
2
3
4
5
6
RSpec.describe 'テスト対象' do
describe '条件' do # ここをdescribeに変えた
it '期待する結果' do
expect(actual).to eq(expected)
end
end
ただし、この二つは意味的な使い分けがあります。
describe
: テストの対象を記述するcontext
: テストの背景(条件)を記述する
また、 describe
と context
はいくらでも入れ子にすることができます。
1
2
3
4
5
6
7
8
9
10
11
RSpec.describe 'aaa' do
describe 'bbb' do
context 'ccc' do
describe 'ddd' do
context 'eee' do
...
end
end
end
end
end
「テストコードは仕様書」ということを意識して、意味的にもわかりやすいテストコードを心がけましょう。
まずは、以下をテストコードにしてみましょう。
fizzbuzzメソッドの引数に 1 を渡した場合、文字列 "1" を返す。
spec/ ディレクトリ直下に、 fizzbuzz_spec.rb を作成し、以下を記述します。
1
2
3
4
5
6
7
RSpec.describe 'fizzbuzzメソッドは' do
context '引数に 1 を渡した場合' do
it '文字列 "1" を返す' do
expect(fizzbuzz(1)).to eq('1')
end
end
end
テストの実行(RED)
以下のコマンドを実行すると、自動テストが流れます。
1
$ bundle exec rspec
当然プロダクトコードを記述していないため、テストは失敗するはずです。
この状態を RED
といいます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ bundle exec rspec
> :
> Failures:
>
> 1) fizzbuzzメソッドは 引数に 3 を渡した場合 "Fizz" を出力する
> Failure/Error: expect(fizzbuzz(3)).to eq('Fizz')
>
> NoMethodError:
> undefined method `fizzbuzz' for #<RSpec::ExampleGroups::Fizzbuzz::Nested3:0x00007fe522a65310>
>
> (中略)
>
> 1 example, 1 failure
> :
Note : テスト実行時のオプション
テスト実行時、テストコードのファイルを指定するとそのファイルだけテストを流すことができます。
1
$ bundle exec rspec spec/fizzbuzz_spec.rb
また -e
または --example
オプションで文字列を指定すると、テストケースに指定された文字列が含まれるものだけテストされます。
1
$ bundle exec rspec -e '引数に 1 を'
この他にも、テストコードの行番号を指定する方法や、前回失敗したものだけ再度テストするなど便利なオプションが存在するのでいろいろ調べてみましょう。
プロダクトコードを書いてテストを通す(GREEN)
プロダクトコード fizzbuzz.rb を実装します。
ファイルは rspec_tutorial/ ディレクトリ直下に置きましょう。
とりあえずテストを通すことが目的ですから、どんな引数がきても文字列 "1" を返すように実装してみます。
1
2
3
def fizzbuzz(natural_num)
'1'
end
このままだとテストコードからプロダクトコードが読めないので、 spec/fizzbuzz_spec.rb に以下の読み込み処理を追加します。
1
2
3
4
5
6
7
8
9
require_relative '../fizzbuzz.rb'
RSpec.describe 'fizzbuzzメソッドは' do
context '引数に 1 を渡した場合' do
it '文字列 "1" を返す' do
expect(fizzbuzz(1)).to eq('1')
end
end
end
それでは、再度テストを実行してみましょう。
問題がなければテストが成功するはずです。
1
2
3
$ bundle exec rspec
> :
> 1 example, 0 failures
本来ならばこのあとプロダクトコードを整形する REFACTORINGがありますが、まだほとんど書いていないのでしばらく飛ばします。
「動く状態でコミット」が理想なので、この時点でコミットしておきましょう。
1
2
$ git add spec/fizzbuzz_spec.rb fizzbuzz.rb
$ git commit -m "最初のテストを追加"
TDDサイクルをまわそう
仕様を元に新たなテストケースを増やす(RED)
引数に 1 を渡した場合のテストが成功したので、引数に 2
を渡した場合にもテストが通るか確認します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require_relative '../fizzbuzz.rb'
RSpec.describe 'fizzbuzzメソッドは' do
context '引数に 1 を渡した場合' do
it '文字列 "1" を返す' do
expect(fizzbuzz(1)).to eq('1')
end
end
context '引数に 2 を渡した場合' do
it '文字列 "2" を返す' do
expect(fizzbuzz(2)).to eq('2')
end
end
end
それではテストを実行してみます。
1
$ bundle exec rspec
現状の実装では常に文字列 "1"
しか返さないので、当然失敗するはずです。
今回の例は非常に簡単なので当たり前のように感じますが、複雑なプログラムになるほどこの「異なる値でも通るか試してみる」というのが重要になります。
このような方法を「三角測量」と呼んだりします。
プロダクトコードを直す(GREEN)
どんな引数がきても文字列に変換して返すようにしてみましょう。
プロダクトコードを書いて…
1
2
3
def fizzbuzz(natural_num)
natural_num.to_s
end
テストを流して…
1
2
3
$ bundle exec rspec
> :
> 2 examples, 0 failures
通ったらコミット。
1
2
$ git add -u
$ git commit -m "数値を文字列に変換して返す"
※ git add -u
の -u
は、すでにgit管理下にあり変更されたファイルのみaddします。
さて、さらにサイクルを回しましょう。
同様の手順で、以下のケースを順番に実装していきましょう。
REFACTORINGは一旦スキップして構いません。
1
引数に 3の倍数 を渡した場合、文字列 "Fizz" を返す
1
引数に 5の倍数 を渡した場合、文字列 "Buzz" を返す
1
引数に 3と5両方の倍数 を渡した場合、文字列 "FizzBuzz" を返す
リファクタリング(REFACTORING)
ここまでのテストがすべて通るようになっていれば、テストコード・プロダクトコードいずれも正しい状態になっているはずです。
この時点で、プロダクトコードを変更する場合にはテストコードが、逆にテストコードを修正する場合にはプロダクトコードが動作を担保するので、リファクタリングが簡単に行なえます。
コードがより簡潔に書けないか見直してみましょう。
Step Up! : 前処理の共通化: let / let! / before
テストケースを書く時に、クラスの初期化といったような「どのテストでも共通で使いたい変数や処理」がある場合があります。
そのようなケースでは、 let
メソッドや before
ブロックを使うとよりシンプルに書ける場合があります。
テストケースに必要となるデータの準備: let / let!
let
メソッドは以下のように書くことで、テストケースで利用できる変数を定義することができます。
1
let(:name) { value }
例えば、テストケースに必要なクラスインスタンスの作成などを行う際に利用可能です。
1
2
3
4
5
6
7
8
9
RSpec.describe 'Humanクラス' do
let(:human) { Human.new(name: 'taro') }
describe 'get_nameメソッドが' do
it '文字列 "taro" を返す' do
expect(human.get_name).to eq('taro')
end
end
end
let
メソッドの注意点として、「呼ばれた時に初めて評価される」という性質があります(これを 遅延評価 と呼びます)。
さらに、一度評価されると、同じ it
ブロックの中では評価値を返します。
これに対して、 let!
メソッドを使うと、処理が定義された行に到達した段階で評価されます( 即時評価 )。
書き方は let
メソッドと同様で、どちらも it
ブロックの外側で定義可能です。
テストケースの前提条件・振る舞い: before
before
ブロックは、let
メソッドなどと同様に、事前に行う処理を記述することができます。
実行タイミングは let!
メソッドと同様の 即時評価 で、「前提条件の設定や振る舞い(メソッド)の実行」を行う役割を持ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RSpec.describe 'Robotクラスの' do
let(:robot) { Human.new(name: 'pepper') }
describe 'robotインスタンスが' do
context '人を見たとき' do
before do
robot.see(human)
end
it '"hello" と挨拶する' do
expect(human.say).to eq('hello')
end
end
end
end
これらの機能は、処理を共通化することで処理を簡潔にできる一方で、使い方を間違えると逆に見るべき箇所が行ったり来たりして可読性を損なう危険もあります。
「テストコードは仕様書」ということを常に念頭に置きつつ、効果的に活用しましょう。
もっと詳しく
RSpecには、より高度かつ効果的なテストを行うための機能が多く備わっています。
詳しくは、公式のドキュメントなどを見てみましょう。