はじめに
この記事は書籍「プロを目指す人のためのRuby入門」に掲載できなかったトピックを著者自らが紹介するアドベントカレンダーの21日目です。
本文に出てくる章番号や項番号は書籍の中で使われている番号です。
今回は第9章の例題で使用した「正規表現チェッカープログラム」のテストを自動化する方法を紹介します。
必要な前提知識
「プロを目指す人のためのRuby入門」の第9章まで読み終わっていること。
正規表現チェッカープログラムのテストを自動化する
正規表現チェッカープログラムのおさらい
第9章では次のような仕様の正規表現チェッカープログラムを作成しました。
- ターミナル上で動作する対話型のCUI(character user interface)プログラムとする
- 起動すると"Text?: "の文言が表示され、正規表現の確認で使うテキストの入力を求められる
- テキストを入力すると、"Pattern?: "の文言が表示され、正規表現パターンの入力を求められる
- 正規表現として無効な文字列だった場合は、"Invalid pattern: "の文言に続いて具体的なエラー内容が表示され、再度パターンの入力を求められる
- 正規表現の入力が終わると、"Matched: "の文言に続いて、すべてのマッチした文字列がカンマ区切りで表示される
- ひとつもマッチしなかった場合は"Nothing matched."の文言が表示される
- マッチング結果を表示したらプログラムを終了する
以下はいくつかの文字列がマッチした場合の実行例(パターンの再入力あり)です。
Text?: 123-456-789
Pattern?: [1-6+
Invalid pattern: premature end of char-class: /[1-6+/
Pattern?: [1-6]+
Matched: 123, 456
こちらはマッチする文字列がなかった場合の実行例です。
Text?: abc-def-ghi
Pattern?: [1-6]+
Nothing matched.
書籍内では実装例として、次のようなRubyプログラムを作成しています。
print 'Text?: '
text = gets.chomp
begin
print 'Pattern?: '
pattern = gets.chomp
regexp = Regexp.new(pattern)
rescue RegexpError => e
puts "Invalid pattern: #{e.message}"
retry
end
matches = text.scan(regexp)
if matches.size > 0
puts "Matched: #{matches.join(', ')}"
else
puts "Nothing matched."
end
「プロを目指す人のためのRuby入門」は原則としてテスト駆動開発(TDD)で例題プログラムを実装するようにしていますが、この例題だけはTDDを使わずに実装していました。
この正規表現チェッカープログラムもテストを書こうと思えば書くことができます。少し難しいですが、以下でその方法を紹介します。
メインプログラムをメソッド化する
まず、メインプログラム(lib/regexp_checker.rb
)をメソッド化します。さらに、コマンドラインから実行された場合のみ、そのメソッドを実行するようにします。
# トップレベルではなく、メソッド内に先ほどのプログラムを記述する
def regexp_checker_main
print 'Text?: '
text = gets.chomp
# (以下省略)
end
# コマンドラインからこのファイルを直接実行した場合だけ、メソッドを実行する
if __FILE__ == $0
regexp_checker_main
end
if __FILE__ == $0
はコマンドラインから自分が直接実行されたかどうかを判定する条件分岐です。__FILE__
には現在のソースファイルのファイルパスが、$0
にはrubyコマンド実行時に指定されたファイルのファイルパスがそれぞれ入っています。この値がどちらも同じであれば、「自分が直接実行された」と見なすことができるわけです。
ピンと来ない場合は次のようにして__FILE__
と$0
の中身を確認してみてください。
def regexp_checker_main
# 省略
end
# __FILE__と$0の中身を確認する
puts "__FILE__: #{__FILE__}"
puts " $0: #{$0}"
if __FILE__ == $0
regexp_checker_main
end
# regexp_checker.rbを直接実行した場合
$ ruby lib/regexp_checker.rb
__FILE__: ./lib/regexp_checker.rb
$0: ./lib/regexp_checker.rb
# テストコードからregexp_checker_mainメソッドを呼びだした場合
$ ruby test/regexp_checker_test.rb
__FILE__: /(ファイルが存在するパス)/lib/regexp_checker.rb
$0: ./test/regexp_checker_test.rb
マッチする結果がある場合のテストコードを書く
では、テストコードに移ります。今回はtest
ディレクトリにregexp_checker_test.rb
というファイルを作ります。以下がそのテストコードです。
require 'minitest/autorun'
require_relative '../lib/regexp_checker'
class RegexpCheckerTest < Minitest::Test
def test_main_when_matched
# ユーザーの入力値
inputs = [
"123-456-789\n",
"[1-6+\n",
"[1-6]+\n"
]
# 期待する標準出力の内容
expected = [
'Text?: ',
'Pattern?: ',
"Invalid pattern: premature end of char-class: /[1-6+/\n",
'Pattern?: ',
"Matched: 123, 456\n"
].join
# スタブを使って標準入力の入力値を偽装する
stub :gets, -> { inputs.shift } do
# regexp_checker_mainを実行して、期待通りの出力が得られるか検証する
assert_output(expected) do
regexp_checker_main
end
end
end
end
これまでのテストコードとは異なり、急に複雑になりましたね。このテストコードが書きにくい理由は標準入力と標準出力をそれぞれプログラムのインプットとアウトプットとしているためです。
スタブを使って標準入力を偽装する
まず、標準入力についてはスタブ(stub)と呼ばれるMinitestの機能を使ってgets
メソッドの戻り値を偽装しています。stub :gets
で始まる行がスタブを設定しているコードです。-> { input.shift }
はラムダと呼ばれるオブジェクトです。ラムダについては第10章で詳しく説明しますが、ここでは「一種のブロックのようなもの」と考えてください。
次に、regexp_checker_main
メソッドの内部でgets
メソッドが呼ばれるたびにラムダ内のコードが実行されます。配列inputs
の内容はshift
メソッドによって前から順番に取り出され、gets
メソッドの戻り値になります(つまりユーザーの入力が偽装されます)。
このようにして、ユーザーが実際に標準入力から文字列を入力したかのように、regexp_checker_main
メソッドをだましています。
assert_outputメソッドで標準出力の内容を検証する
標準出力の内容はMinitestで用意されているassert_output
メソッドを使って検証します。assert_output
メソッドは渡されたブロックが実行し、そこで出力された標準出力の内容がassert_output
メソッドの第1引数(ここでは変数のexpected
)と一致すれば、テストがパスします。
気を付けなければいけないのは改行文字の扱いです。メインプログラム内ではprint
メソッドとputs
メソッドの2種類を使っていますが、改行文字が入るのはputs
メソッドだけです。なので、puts
メソッドで出力した文字列だけが標準出力上でも改行されます。また、標準入力から入力した文字列("123-456-789"など)は標準出力への出力値とは見なされません。こうしたことから、変数expected
に設定した文字列は実際のターミナル上で目に見える文字列とは少し異なっています。
配列 + joinでexpectedの文字列を作成した理由
なお、上のコードではexpected
の文字列を作成するために、一度配列に入れた文字列をjoin
メソッドで連結しています。expected
の中身は次のようなひと続きの文字列になります。
expected = [
'Text?: ',
'Pattern?: ',
"Invalid pattern: premature end of char-class: /[1-6+/\n",
'Pattern?: ',
"Matched: 123, 456\n"
].join
expected
#=> "Text?: Pattern?: Invalid pattern: premature end of char-class: /[1-6+/\nPattern?: Matched: 123, 456\n"
配列とjoin
を使わずに、最初から次のようにひと続きの文字列を代入しても結果はもちろん同じです。
expected = "Text?: Pattern?: Invalid pattern: premature end of char-class: /[1-6+/\nPattern?: Matched: 123, 456\n"
ですが、ひと続きの文字列だと処理の区切りが分かりづらいと考えたため、処理の区切りが見た目にわかりやすくなるよう、あえて配列に入れて最後にjoin
するようにしています。
何もマッチしなかった場合のテストコードを書く
同じ要領で、何もマッチしなかった場合のテストを書くこともできます。以下はそのテストコードです。
class RegexpCheckerTest < Minitest::Test
# 省略
def test_main_when_not_matched
inputs = [
"abc-def-ghi\n",
"[1-6]+\n"
]
expected = [
'Text?: ',
'Pattern?: ',
"Nothing matched.\n"
].join
stub :gets, -> { inputs.shift } do
assert_output(expected) do
regexp_checker_main
end
end
end
end
Ruby初心者の方は今回紹介したテストコードを最初から完全に理解したり、自分で書いたりするのは難しいと思います。なので、今の時点では「こんなやり方もできるんだ」という雰囲気だけなんとなくつかんでもらえれば、それで構いません。
次回予告
次回はHash#to_proc
とMethod#to_proc
について説明します。