伊藤淳一さんとは
Rubyistには馴染みの深いRspecだけど、そのRspecの第一人者であるのが伊藤淳一さんだ。
日本生まれのRubyだがそのテスティングフレームワークであるRspecの専門書を日本語で書いている人である(翻訳)。
Rspecの本を一度でも探したことがあるならどれほど有名な人かはわかるのだが、とにかく彼の書いた「Everyday Rails - RSpecによるRailsテスト入門」が人気でこれだけでも彼の残した功績は大きい(らしい)。
Rspecとは
Rspecというのは、Rubyの開発現場でよく使われるテスティングフレームワークだ。
テスティングフレームワークというのは、テストをするためのフレームワークと言う意味で、テストと言ってもここでは単体テスト(Unit Test(UT))のことを指している。
単体テストとは、誤解を恐れず言えばモジュール(つまり1機能を有したファイル群のこと)単位でソースコードのロジックが要件通りに動くかどうかを確認するテストのことだが、これを自動化させられるのがRspecというわけだ。
ちなみに同じ単体テスト用のフレームワークとして、Minitestというのがある。
これは現在Rubyの標準で付属されているフレームワークで、記述もRspecのようにDSL(ドメイン固有言語)でなくRubyで書かれている。
つまりRubyが書ければテストも同時に書くことができるという意味で学習コストが低い。
さらにRspecの特徴として、ビヘイビア駆動開発(behavior driven development(BDD))というものが挙げられる。
ざっくり言えば、メソッドなどの振る舞い(=Behaviour)に焦点を当ててテストをする方法だ。
RspecはこのBDDに特化したテスティングフレームワークなのである。
テスト全般に関しては以下のQiitaのエントリがわかりやすい。
ちなみにRspecは単体テスト用のテストツールということだが、結合テスト以降ももちろんテストのフレームワークはある。
ちなみに結合テストはエンドツーエンド(End-to-End)テストとも言われ、こっちの末端からあっちの末端までというモジュール間を繋いだ、実際の動作に近いテストのことだ。
Rubyではひと昔前はCucumberというフレームワークだったようだが、現在はTurnipというものが主流らしい。
ちなみにブラウザテスト(統合テストに近いかな)ではSeleniumというフレームワークが有名だ。
こちらもRspecでの記述が可能である。
結構古いがこれわかりやすい[中級者向け]
エンドツーエンドテストの自動化は Cucumber から Turnip へ
プログラミング初心者を悩ますテストの違い[初心者向け]
[testing] システムテストとエンドツーエンドテストの違い [terminology] [qa] | CODE Q&A 問題解決 [日本語]
テスト駆動開発とは
編集中
早速やってみる
では早速だが、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
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とも少し書き方が異なる。
上記の四則演算を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
さて、新しい構文が出てきたな。
まず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という構文で書くと、インスタンス変数を書かなくてもよくなっている。
先ほどハッシュにname: 'たろう'を配列の要素として持たせたわけだが、それにさらにmerge!でageを追加して上書きしていることがわかる。
さらにこのテストコードをリファクタリングしてみる。
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
letを使うことでテストコードがかなりコンパクトになったことがわかっただろうか。
先ほどmerge!メソッドで破壊的にハッシュを上書きしていたが、ageを変数にしてcontext内の値を取得するようにしてよりすっきりした。
ちなみにbeforeに比べて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
書いてわかること
テストを書くメリットは以下の通りだ。
- 1回書くだけで何度でもテストができる
- ソースコードの精度が上がる
- CI(継続的インテグレーション)との相性がいい
用語的な何か
example
テストの最小単位のことdescribe
テスト対象を指す。
複数のテストをまとめて、その中で対象を分けるときはネストする。context
条件を指定するときなどに使う、ON/OFFやTrue/falseのときなど分岐に使う。
describeのエイリアス。
describeが対象を指すなら、contextは状況を指す。before
そのテストをする前の処理として記述する。after
beforeの逆。後に何かの処理をさせるときに記述する。it
実際のテストを記載するときに使う。
マッチャなどと一緒に使うことが多い。should
逆はshould_notexpect_to
subject
レシーバを指定するときに使う。its
shared_examples
let
ブロックの評価を引数のSymbolとして利用できる。double
スタブを作るときに使用するstub
doubleで作成したスタブにメソッドを追加できる。 もちろんオブジェクトやクラスにも使える。say
モック作成のときに使う。(should_)receive
sayでメソッドが呼ばれたら、ここで定義した値を返す。