概要
ユニットテスト(単体テスト)をあまり書いたことがない人が、RubyのテスティングフレームワークであるRSpecを用いてユニットテストの第一歩を踏み出すお話。
本記事では、RSpecを使って極単純なクラスの各種メソッドについてのユニットテストを作成する。
目次
前提
以下環境で動作確認
要素 | バージョン |
---|---|
debian | 8.6 |
ruby | 2.2.2 |
rspec | 3.7.0 |
ユニットテストとは
ユニットテストとは、ソースコードの個々のユニット、すなわち、1つ以上のコンピュータプログラムモジュールが使用に適しているかどうかを決定するために、関連する制御データ、使用手順、操作手順とともにテストする手法である[1]。ユニットとはアプリケーションのテスト可能な最小の部品単位である、と直観的にとらえることができる
端的にいうと、クラス及び各種メソッドが、仕様通りに動作しているかを機械的に検証する仕組み。
ユニットテストを書くとどんな旨味があるのかは以下の通り
- コード修正時の影響範囲を検証できるので、デグレーションなどを防げる
- テストコードを書きやすくするためには、元のユニットもシンプルにわかりやすく書き上げる必要があるので、コード品質が高まる
- テストコードを書く時はユニットの利用者になるので、利用者的に使いやすいコードを書けるようになる
- クラス/メソッドの使い方とその結果がテストコードに記述されるので、ドキュメントにもなる
などなど。
「テスト」というからには「正しく動いていることを確認する」ことに目的を起きがちだが、実はテストを書くことで元のコードを高品質に書けるようになるという大きなメリットがあるようだ。
RSpecとは
Rubyでユニットテストを作成する手段はいくつもあり、minitest/unitはRubyに標準で含まれている(2.0系)ので、すぐに利用できる。
本記事では、その中でも人気のRSpecを用いてユニットテストを作成する。
RSpecの主な特徴は以下の通り
- Rubyで最もポピュラーなテスティングフレームワーク
- メタ視点でのテストが書けるため、テストコードが自然言語っぽくなる(※但し英語を用いた場合に限る)
- 初期はRailsでも標準のテスティングフレームワークだった(学習コストを考慮して現在ではminitestになってるみたい)
- ユニットテストに限らず、結合テストやブラウザテストなどにも使える
- ビヘイビア駆動開発を想定したやや独特な記法を用いるので学習コストが微妙に高い
なんともしっくりこない特徴がチラホラあるが、本記事ではあまり影響しないので気にせず使う。
RSpecのインストール
RSpecはgemでインストールできる。今回はグローバルインストールするので以下のようにインストールする。
$ gem install rspec
グローバルコマンドで使えるようになる
vagrant$ rspec -v RSpec 3.7 - rspec-core 3.7.0 - rspec-expectations 3.7.0 - rspec-mocks 3.7.0 - rspec-support 3.7.0
Rspecの初期化
作業ディレクトリにて、以下コマンドを実行するとRspecに関する初期ディレクトリ、ファイルを生成してくれる
$ rspec --init
specディレクトリが作成され、中にspec_helper.rbが作成される。このファイルはRspecの振る舞いを定義した設定ファイルで、デフォルトで色々記述されているが、本記事では特に編集しないのでそのまま利用する。
vagrant$ tree -a . ├── .rspec └── spec └── spec_helper.rb 1 directory, 2 files
テスト対象クラスの作成
今回は初めてのユニットテストなので、極シンプルな以下のクラスを実装する。以下Calcクラスは、オブジェクト生成後、各種メソッドを用いて、足し算/引き算/かけ算/割り算を行うことができる。
class Calc def add(a, b) a + b end def subtract(a, b) a - b end def multiply(a, b) a * b end def divide(a, b) a / b end end
これを、calc.rbというファイル名で、srcディレクトリ直下に配置する。
また、これから作成する、Calcクラスをテストするテストスクリプト(calc_spec.rb)を、specディレクトリ直下に配置する。
ディレクトリ構造は以下の通り。
vagrant$ tree . ├── spec │ ├── calc_spec.rb │ └── spec_helper.rb └── src └── calc.rb 2 directories, 4 files
以降では、このClacクラスについてのユニットテストをcalc_spec.rbに記述する。
足し算メソッドをテストする
まずは、前項で作成したCalcクラスの、addメソッドについてテストする。
addメソッドは、引数a,bの和を戻すので理論上は1と1を投げれば1 + 1で2が返ってくるはずだ。これをテストする。
require 'rspec' require_relative '../src/calc' RSpec.describe Calc do it "足し算できること" do expect(Calc.new.add(1, 1)).to eq 2 end end
細かいことは学びながら身に付けていけば良いが、describeはテスト対象を、itは期待する振る舞いに関する説明を宣言して、その中にテストコードを記述する。
expect(a).to eq b
は、aがbと一致していることを検証する手法で、本記事ではこれしか使わないのでとりあえずここを抑えておけばよい。
テストの実行はrspecコマンドをそのまま叩くことで実行できる。rspecコマンドは、作業ディレクトリ以下のテストスクリプトを探索し、それを実行して結果を標準出力する。
vagrant$ rspec Randomized with seed 26931 Calc 足し算できること Top 1 slowest examples (0.00037 seconds, 6.6% of total time): Calc 足し算できること 0.00037 seconds ./spec/calc_spec.rb:5 Finished in 0.00556 seconds (files took 0.09292 seconds to load) 1 example, 0 failures Randomized with seed 26931
初めて見ると、出力が色々出てきて混乱するかもしれないが、
1 example, 0 failures
を見ると、1個のテストを実施して0個失敗した(=全て成功した)ことが確認できる。
ちなみに
Randomized with seed 26931
は、今回のテストの乱数シードを表す。今回は一つのテストしか無いが、基本的にテストは複数のテスト項目に対してまとめて実施する。その場合の実施順序を毎回ランダムにすることで、各ユニットが実行順に依存しないことを証明できる。
足し算クラスのバグを検知する
例えば、誰かがCalcクラスを更新して、足し算メソッドが以下のように変わってしまったとする。
def add(a, b) a - b end
本来は2数の和を戻すメソッドなのに、2数の差が戻ってしまう。これは紛れもなくバグである。
この状態で再度テストを実施すると
vagrant$ rspec Randomized with seed 58714 Calc 足し算できること (FAILED - 1) Failures: 1) Calc 足し算できること Failure/Error: expect(Calc.new.add(1, 1)).to eq 2 expected: 2 got: 0 (compared using ==) # ./spec/calc_spec.rb:6:in `block (2 levels) in <top (required)>' Top 1 slowest examples (0.00991 seconds, 64.0% of total time): Calc 足し算できること 0.00991 seconds ./spec/calc_spec.rb:5 Finished in 0.01547 seconds (files took 0.09346 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/calc_spec.rb:5 # Calc 足し算できること Randomized with seed 58714
のようになり、どのテストで失敗したのか、期待した結果と実際に得られた結果がどう違ったのかなどを確認することができ、迅速にコードの修正を行うことができる。
複数のテストをまとめて実行する
同様に、引き算/かけ算/割り算についてのテストも記述する。
RSpec.describe Calc do it "足し算できること" do expect(Calc.new.add(1, 1)).to eq 2 end it "引き算できること" do expect(Calc.new.subtract(2, 1)).to eq 1 end it "かけ算できること" do expect(Calc.new.multiply(3, 2)).to eq 6 end it "割り算できること" do expect(Calc.new.divide(10, 2)).to eq 5 end end
同じようにrspecコマンドでテストを実行すると、全てのテストが実行され、問題ないことが確認できる。
vagrant$ rspec Randomized with seed 39869 Calc かけ算できること 足し算できること 引き算できること 割り算できること Top 4 slowest examples (0.00071 seconds, 9.1% of total time): Calc かけ算できること 0.00039 seconds ./spec/calc_spec.rb:11 Calc 足し算できること 0.00011 seconds ./spec/calc_spec.rb:5 Calc 割り算できること 0.00011 seconds ./spec/calc_spec.rb:14 Calc 引き算できること 0.0001 seconds ./spec/calc_spec.rb:8 Finished in 0.00781 seconds (files took 0.08813 seconds to load) 4 examples, 0 failures Randomized with seed 39869
少しだけメソッドを複雑化する
足し算メソッド、引き算メソッドそれぞれをほんの少し改変して、以下のような仕様にする
- 足し算メソッド: 和が10を超えた場合、nilを戻す
- 引き算メソッド: 差が0未満の場合、nilを戻す
改変したコードが以下の通り。
※ ここからは簡略化のために、足し算メソッド/引き算メソッドのみにします。
class Calc # # 2数の和を戻す # 但し10を超えた場合nilを戻す # def add(a, b) a + b <= 10 ? a + b : nil end # # 2数の差を戻す # 但し0未満の場合nilを戻す # def subtract(a, b) 0 <= a - b ? a - b : nil end end
境界値分析を行う
足し算メソッドは、和が10を超えた場合にnilを返す。
となると、分岐の境界になる、和が10、もしくは11になる入力のケースが最もバグが生まれやすいと考えられる。
同様に、引き算メソッドは0が境界になるので、差が0、もしくは-1になる入力のケースが怪しい。
以上のような境界値分析を考慮して、以下のようにテストを追加する。
RSpec.describe Calc do describe "足し算メソッド" do it "通常ケースで和が戻る" do expect(Calc.new.add(5, 3)).to eq 8 end it "和が10の場合は10が戻る" do expect(Calc.new.add(8, 2)).to eq 10 end it "和が10を超えた場合はnilが戻る" do expect(Calc.new.add(8, 3)).to eq nil end end describe "引き算メソッド" do it "通常ケースで差が戻る" do expect(Calc.new.subtract(5, 3)).to eq 2 end it "差が0の場合は0が戻る" do expect(Calc.new.subtract(3, 3)).to eq 0 end it "差が0未満の場合はnilが戻る" do expect(Calc.new.subtract(3, 4)).to eq nil end end end
describeは、テスト対象を定義すると述べたが、これは入れ子で表現することもできる。
そのため、Calcに対するテストの中に、「足し算に関するテスト」「引き算に関するテスト」を入れ子でdescribeに定義したことで、何のテストを行っているのかをよりわかりやすくした。
テストを実行した結果は以下の通り
vvagrant$ rspec Randomized with seed 57848 Calc 足し算メソッド 和が10の場合は10が戻る 和が10を超えた場合はnilが戻る 通常ケースで和が戻る 引き算メソッド 差が0未満の場合はnilが戻る 通常ケースで差が戻る 差が0の場合は0が戻る Top 6 slowest examples (0.00096 seconds, 9.3% of total time): Calc 足し算メソッド 和が10の場合は10が戻る 0.00031 seconds ./spec/calc_spec.rb:9 Calc 足し算メソッド 和が10を超えた場合はnilが戻る 0.00015 seconds ./spec/calc_spec.rb:12 Calc 引き算メソッド 差が0未満の場合はnilが戻る 0.00014 seconds ./spec/calc_spec.rb:23 Calc 足し算メソッド 通常ケースで和が戻る 0.00013 seconds ./spec/calc_spec.rb:6 Calc 引き算メソッド 差が0の場合は0が戻る 0.00012 seconds ./spec/calc_spec.rb:20 Calc 引き算メソッド 通常ケースで差が戻る 0.00012 seconds ./spec/calc_spec.rb:17 Finished in 0.01034 seconds (files took 0.10127 seconds to load) 6 examples, 0 failures Randomized with seed 57848
以上のように、全てのテストが通ったことが確認できた