単項マイナスと構文解析
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
今回もminitestの話です。 mockとstubに焦点をあて説明します。
今回は単語帳プログラム「wordbook」を、テストしながら作ることにします。 このプログラムは、テストの例示に使うためのものなので、最小限の機能に絞りました。
端末からの入力に従って、単語帳を編集し、ファイル「に保存/から読み出し」できるというものです。 端末からの入力は「コマンド 英語 日本語訳」という形を原則にしています。
コマンドはこの5つだけです。
wb > a add 追加 #=> 英単語「add」と日本語訳「追加」をデータに追加
wb > p add #=> 英単語「add」を表示
add 追加
wb > c add 加える #=> 「add」の訳を「加える」に変更
wb > p add
add 加える
wb > a subtract 減じる
wb > p . #=> 正規表現が可能。任意の文字にマッチ(マッチは単語の一部で良い)
add 加える
subtract 減じる
wb > d add #=> 「add」とその日本語訳を削除
wb > p .
subtract 減じる
wb > q #=> 終了
プログラム・ファイルは4つに分かれます。
開発は、トップダウンで行うことにします。 トップダウンとは、メインになるプログラムから開発し、メインから呼び出される個々のパーツ・プログラムを後に回す方法です。 逆の手順はボトムアップです。 ボトムアップの利点はひとつひとつ動くパーツから組み立てるので、着実に積み上げることができることです。 ただ、メイン部分で問題が発生すると、また下位のパーツを作り直さなければならなくなるという不利な点があります。
コマンドラインとのインターフェースはwordbook.rbに書きます。 このプログラムは、起動時の引数の処理をします。
プログラムは次のようになります。
#!/bin/sh
exec ruby -x "$0" "$@"
#!ruby
require_relative 'lib_wordbook.rb'
def usage
$stderr.print "Usage: wordbook [file]\n"
exit
end
if ARGV.size > 1 || ARGV[0] =~ /--help|-h/
usage
end
if ARGV.size == 1
wb = WordBook.new(ARGV[0])
else
wb = WordBook.new
end
wb.run
$stderrは標準エラー出力のオブジェクトを表す変数でprintメソッドを持っています。 このメソッドは関数形式のprintメソッドと同じで、出力先が違うだけです。 –helpと-hは使い方を表示して終了します。 exitはプログラムを終了するメソッドです。
正しい引数で起動された場合は、WordBookクラスのインスタンスを生成し、そのオブジェクトのrunメソッドを呼び出します。 runメソッドが実質的なメインプログラムになります。
wordbook.rbはコマンドライン引数の解析をするので、テストもコマンドラインから起動して行いたいところです。 そこで、Kernelモジュールのバックティック(`)メソッドを利用して、rubyを実行し、その標準出力を入手してテストに用いることにします。 バックティック・メソッドは「Kernelモジュール」を参照してください。 テストプログラムのファイル名は「test_main_wordbook.rb」とします。
require 'minitest/autorun'
require 'fileutils'
# The test will be done under 'temp_test_main_wordbook' directory
class TestMainWordbook < Minitest::Test
include FileUtils
def setup
@tempd = 'temp_test_main_wordbook'
mkdir_p @tempd
cp 'wordbook.rb', "#{@tempd}/wordbook.rb"
# Put a stub of "lib_wordbook.rb" under the tepmorary directory.
# It just prints the argument.
File.write("#{@tempd}/lib_wordbook.rb", <<~'EOS')
class WordBook
def initialize(file="db.csv")
@file = file
end
def run
print @file, "\n"
end
end
EOS
cd @tempd
end
def teardown
cd '..'
remove_entry_secure @tempd
end
def test_main_wordbook
assert_equal("Usage: wordbook [file]\n", `ruby wordbook.rb --help 2>&1`)
assert_equal("Usage: wordbook [file]\n", `ruby wordbook.rb -h 2>&1`)
assert_equal("Usage: wordbook [file]\n", `ruby wordbook.rb a.csv b.csv 2>&1`)
assert_equal("db.csv\n", `ruby wordbook.rb`)
assert_equal("abc.csv\n", `ruby wordbook.rb abc.csv`)
end
end
コマンドラインからruby wordbook.rb
と入力すると、wordbook.rbはlib_wordbook.rbを読み込み、WordBookクラスのインスタンスを作ろうとします。
まだ、lib_wordbook.rbは書いていませんし、またそれが書けていたとしてもテストには向きません。
ここでは、テスト用のlib_workbook.rbを使ってテストしたいので、新たにテンポラリ・ディレクトリ(一時ディレクトリ)を作り、その中でテストをすることにします。
テンポラリ・ディレクトリ名は「temp_test_main_wordbook」とします。
setupメソッドで上に述べた「下準備」をします。
setupと対になるのがteardownで、これはテスト終了後の後始末をします。 teardownでは次のことを行います。
テストをするメソッドは「test_main_wordbook」です。
2>&1
は標準エラー出力の出力先を標準出力に変更する(bashのリファランス参照)。
バックティックはコマンドの標準出力を捕らえ、メソッドの返り値にするテストしてみます。
$ ruby test_main_wordbook.rb
Run options: --seed 23981
# Running:
.
Finished in 0.391359s, 2.5552 runs/s, 12.7760 assertions/s.
1 runs, 5 assertions, 0 failures, 0 errors, 0 skips
$
トップレベルのファイルはどうしてもコマンドラインの解析があるので、テスト用のライブラリファイル(このようなものをスタブという)を作り、テスト用のテンポラリディレクトリでテストする形になります。 これは私流のやりかたですが、もし他にもっと良い方法をご存知の方がいれば、コメントで教えていただけるとありがたいです。
Minitestのstubは「Objectクラスに追加したメソッド」です。 すべてのクラスはObjectの子孫ですから、どのオブジェクトの上でもstubメソッドを呼ぶことができます。 また、「クラスも一種のオブジェクト」ということから、クラスの上でもstubメソッドを呼ぶことができます。
stubはオブジェクトの既存のメソッドの返り値を変更することができます。
オブジェクト.stub(メソッド名, 返り値){ ・・・・・}
このような形で使います。 正確には、引数にさらに付け加えられる情報があるのですが、詳細はMinitestのドキュメントをご覧ください。
stubはどんなオブジェクトに対しても使えるので、とくに入力関係のオブジェクトに使うと効果的です。 例えばFileクラスのクラスメソッドreadに対して、
File.stub(:read, "abcd\n") {・・・・・}
とすると、{}
の中、すなわちブロックの中ではFile.read(ファイル名)
はいつも”abcd\n”を返します。
stubによるメソッドの変更はブロックの中だけで有効です。
モックは「みせかけのもの」という意味です。 本当のオブジェクトではなく、テストのためにそれらしい振る舞いをするオブジェクトのことをいいます。 minitestのモック・オブジェクトでは、みせかけのインスタンスメソッドとその引数、返り値を定義することができます。
という手順でテストをします。
require 'minitest/autorun'
class TestFoo < Minitest::Test
def test_foo
@mock = Minitest::Mock.new
@mock.expect(:read, "Hello world!")
assert_equal("Hello world!", @mock.read)
@mock.verify
end
end
実際のテストでは、モックオブジェクトを本来のオブジェクトに差し替えてテストをします。 差し替えをどのように行うかは対象となるプログラムによりますが、結構難しくなる場合もあります。 対象プログラムの中身に立ち入らないのがテストの原則ですが、オブジェクトの差し替えはどうしても原則どおりには行かないことが多いと思います。 そのときは、中身に関する事柄をできるだけ少なくします。
スタブとモックを組み合わせて使うこともよくあります。 それは、スタブの2番めの引数(書き換えられたメソッドの返り値)にモックを置くことです。 そのことによって、モックをテスト対象のオブジェクトに送り込むのです。 これは、newメソッドをスタブで書き換え、newで返すオブジェクトをモックに取り替えてしまう、という方法で用いられます。
スタブのより柔軟で高度な使い方としては、2番めの引数(返り値)のところに、callメソッドを持つオブジェクトを置く方法があります。 このときスタブはcallメソッドを実行し、その値を返り値にします。 ここにはProcオブジェクトを入れるのがピッタリですが、モックを入れることも考えられます。 つまり、モックに「みせかけのメソッド」としてcallを定義するのです。 モックは複数回expectを使い、callメソッドの返り値をその回数分セットすることができます。 ということは、スタブで書き換えたメソッドに複数回分の異なる返り値をセットすることが可能になるのです。
require 'minitest/autorun'
# sample class
class A
def initialize
@b = B.new
end
def show_b
@b.show
end
end
class B
def show
"class B のオブジェクトです\n"
end
end
class TestStubAndMock < Minitest::Test
def test_stub_and_mock
@a = A.new
assert_equal("class B のオブジェクトです\n", @a.show_b)
@mock = Minitest::Mock.new
B.stub(:new, @mock) do
@a = A.new
end
@mock.expect(:show, "ぼくはモックだよ!\n")
@mock.expect(:show, "わたしはモックよ!\n")
assert_equal("ぼくはモックだよ!\n", @a.show_b)
assert_equal("わたしはモックよ!\n", @a.show_b)
@mock.verify
end
end
この例では、クラスAのインスタンス生成時にクラスBのインスタンスを作って@bに代入します。
クラスAのshow_bメソッドでは、@b.show
によってクラスBのshowメソッドが呼ばれ”class B のオブジェクトです\n”が返されます。
ちょっと入り組んでいますが、良いでしょうか。
テストプログラムtest_stub_and_mock
の最初の2行は今述べたことを実行して、@.show_b
によって上述の文字列が返されたことを確認しています。
これは正しく動作し、テストはパスします。
メソッドの3行目から6行目では、モックオブジェクト@mockを生成し、stubメソッドによって、B.new
の返り値を@mockにします。
本来B.new
はクラスBのオブジェクトを返すのですが、モックを返すようになっているのです。
これによって、クラスAのオブジェクト@a上ではクラスBの振る舞いがモックの振る舞いに置き換わってしまいます。
次の2行はモックのshowメソッドが返す値を設定しています。
@a.show_b
の中で、@b.show
を実行しますが、@bにはクラスBのオブジェクトではなく、モックが入っているので返り値がモック設定のものになります。
そこで、2つのassert_equalが成功し、最後のverifyも予定通り2回呼ばれていたので成功します。
テストを実行するとすべてパスします。
この方法がクラスAで想定しているのは、initializeメソッドでB.new
が呼ばれるだろうということだけです。
それがクラスAのリファクタリングで変更される可能性はごく小さいはずなので、テストはリファクタリング後も使える可能性が高いといえます。
それでは、次のセクションで単語帳プログラムの実例を見てみましょう。
lib_wordbook.rbではWordBookクラスを定義します。 このクラスは、InputクラスとDBクラスのインスタンスを生成します(それぞれ@inputと@db)。 WordBookクラスのrunメソッドは、これらのインスタンスを使い、次のような動作をします。
q
ならば、ループを抜け出すとともにrunメソッドを抜け出すプログラムは次のようになります。
require_relative 'input.rb'
require_relative 'db.rb'
class WordBook
def initialize(*file)
@input = Input.new
if file[0]
@db = DB.new(file[0])
else
@db = DB.new
end
end
def run
while true
a = @input.input #=> an array like [command, English, Japanese]
return unless a
case a[0]
when 'a'
@db.append(a[1], a[2])
when 'd'
@db.delete(a[1])
when 'c'
@db.change(a[1], a[2])
when 'p'
d = @db.list(a[1]).to_a
d.each do |e,j|
print "#{e} - #{j}\n"
end
when 'q'
@db.close # save data
break
end
end
end
end
クラスから生成されるインスタンスの初期化はinitializeメソッドで行います。
このメソッドの引数が*file
となっているのは、可変長引数を表します。
呼び出し側が、ファイルを引数にする場合と、引数なしの場合があるので、可変長にしました。
引数は配列の形でパラメータfileに代入されます。
実際には引数はあったとしてもひとつで、それはfile[0]
に代入されています。
その引数があれば、それを引数にしてDBクラスのインスタンスを生成します。
引数が無ければ(f[0]==nil
)、引数なしでDBクラスのインスタンスを生成します。
また、Inputクラスのインスタンスも作ります。
runメソッドはwhile true
の無限ループ内で、入力に応じた@dbのメソッドを呼ぶだけです。
pコマンドの時だけ、@dbから得たデータを標準出力に出力するのが、唯一自分自身の仕事になっています。
さて、このファイルをテストする段階で、まだinput.rbとdb.rbはできていません。
require_relative
でエラーにならないように、空のファイルを置いているだけです。
それらのファイルが定義するInputクラスとDBクラスはテストプログラムの中で定義されます。
また、それらのメソッドはモックの「みせかけのメソッド」になります。
以下はtest_lib_wordbook.rb
のプログラムリストです。
require 'minitest/autorun'
require_relative 'lib_wordbook.rb'
# dummy class
class Input
end
class DB
def initialize(*file)
end
end
class TestLibWordbook < Minitest::Test
def test_run
@mock_input = Minitest::Mock.new
@mock_db = Minitest::Mock.new
Input.stub(:new, @mock_input) do
DB.stub(:new, @mock_db) do
@wordbook = WordBook.new
end
end
args = []
args << [['a', 'append', '付け足す'], :append, nil, ['append', '付け足す']]
args << [['d', 'append'], :delete, nil, ['append']]
args << [['c', 'append', '付け加える'], :change, nil, ['append', '付け加える']]
args << [['p', 'app...'], :list, [['append', '付け加える']], ['app...']]
args.each do |a|
@mock_input.expect(:input, a[0])
@mock_db.expect(a[1], a[2], a[3])
@mock_input.expect(:input, ['q'])
@mock_db.expect(:close, nil)
if a[0][0] == 'p'
assert_output("append - 付け加える\n") {@wordbook.run}
else
@wordbook.run
end
@mock_input.verify
@mock_db.verify
end
end
end
テストプログラムについて説明します。
InputとDBクラスを定義しておきます。 これらはテスト用のダミーです。 なお、DBクラスのnewメソッド呼び出しには引数がある場合と無い場合があるので、initializeメソッドの引数にはアスタリスクを付けて可変長にします。
WordBookクラスのinitializeメソッドでInput、DBクラスのインスタンスが@inputと@dbに代入されます。
テストではそれらにモックを入れるために、stubメソッドで両クラスのnewメソッドの返り値をモックに変えてWordBook.new
を実行します。
これで、runメソッドで使う@inputと@dbがモックオブジェクトを表すようになります。
test_runメソッドがテスト本体です。
まず、argの配列を作ります。
4行あるのが、それぞれ、a、d、c、pのコマンドを入力するときの諸データを配列にしたもので、それが<<
メソッドでargに追加されていきます。
最初のデータがeachメソッドのループでどのように使われるかを見ていきましょう。
a[0]=['a', 'append', '付け足す'] 'なので、まず
@wordbook.input.expect(:input, a[0])で、@inputのモックがinputメソッドに対し'['a', 'append', '付け足す']'を返すように定義をします。
これにより、@input.inputが呼ばれた時に
[‘a’, ‘append’, ‘付け足す’]`が返されます。a[1] = :append
、a[2] = nil
、a[3] = ['append', '付け足す']
なので、@wordbook.db.expect(a[1], a[2], a[3])
のところでは、@dbのモックがappendメソッドに対し、返り値nilで引数が'append', '付け足す'
となるように定義をします。
返り値はrunメソッド内では使われていないので、nil以外のものでも構いません。@wordbook.input.expect(:input, ['q'])
で次の@input.inputメソッドの返り値を'q'
にします。
これはrunメソッドの2回めのループでの呼び出しです。@wordbook.db.expect(:close, nil)
で、@db.closeが引数なしで呼び出されるよう定義します。a[0][0]
は'a'
でしたから、else節が実行され、@wordbook.run
すなわちrunメソッドが実行されます。
このなかで@input.input、@db.append、@input.input、@db.closeがこの順で呼ばれるはずです。@wordbook.input.verify
で@inputに代入されたモックが、設定されたメソッドを呼んだかをチェックします。@wordbook.db.verify
で@dbに代入されたモックが、設定されたメソッドを呼んだかチェックします。以上が1セットでこれを内容を変化させて全部で4セット行います。 実行すると、
$ ruby test_lib_wordbook.rb
Run options: --seed 3358
# Running:
.
Finished in 0.005851s, 170.9253 runs/s, 170.9253 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
無事にテストが通過しました。 内容が複雑でしたが、大丈夫でしょうか。
モックが下位プログラムの代わりをしてくれた、ということが大事な点です。
さて、このテストプログラムではWordBook.new
でInputクラスとDBクラスのインスタンス生成が行われていると仮定しました。
これが将来のリファクタリングで変更される可能性は小さいですが、もし変更されればテストプログラムの変更もしなければなりません。
それは、テストプログラムがWordBookクラスの内容に(わずかですが)立ち入っているために起こることです。
すなわち、テストは「対象の振る舞いにフォーカスする」「内部構造に立ち入らない」という原則に触れていることになります。
これを原則に忠実なテストに置き換えるには、モックを諦めなければならないと思います。 なぜならモックはインスタンスの置き換えだからです。
代案としては「クラスのスタブを作る」方法があります。 ここでいうスタブとは、代用品のことで、本物のInputクラス、DBクラスではなく、テスト用に作るものです。 スタブにはテストに必要なすべてのメソッドを持たせ、テストに適するような出力をさせます。 このプログラムは分かりやすく、単純化されます。 モックよりもずっと簡単なので、勧められる方法です。
require 'minitest/autorun'
require_relative 'lib_wordbook.rb'
# dummy class
class Input
def initialize
@count = -1
end
def input
@count += 1
[['a', 'append', '付け足す'], ['d', 'append'], ['c', 'append', '付け加える'], ['p', 'app...'], ['q']][@count]
end
end
class DB
def initialize(*file)
end
def append(e,j)
print "append(#{e}, #{j})\n"
end
def delete(e)
print "delete(#{e})\n"
end
def change(e,j)
print "change(#{e}, #{j})\n"
end
def list(e)
print "list(#{e})\n"
end
def close
print "close\n"
end
end
class TestLibWordbook < Minitest::Test
def setup
@wordbook = WordBook.new
end
def test_run
expected_output = "append(append, 付け足す)\ndelete(append)\nchange(append, 付け加える)\nlist(app...)\nclose\n"
assert_output(expected_output) {@wordbook.run}
end
end
inputメソッドは、カウンタを使って呼ばれるたびに異なる値を返します。 DBの各メソッドは呼ばれるたびに、メソッド名と引数を標準出力に書き出します。 テスト本体ではrunメソッドの出力結果(上記のDBお各メソッドの出力のトータル)と期待される文字列を比較するだけです。 このテストの良いところは
ということです。
このセクションでは、モックを使ったプログラムを書きましたが、それはモックの説明をしたかったからです。 実際にはモックを使わないプログラムの方が適切なテストプログラムだと私は思います。 テストには決まった方法がありません。 いろいろな方法が可能なので、その中で最も良いものをチョイスしてください。
入力を担当するInputクラスの書かれたファイルinput.rbは次のようになります。
require 'readline'
class Input
def input
while true
buf = Readline.readline("wb > ", false)
if buf =~ /^[ac] +[a-zA-Z]+ +\S+$|^d +[a-zA-Z]+$|^p +\S+$|^q$/
return buf.split(' ')
else
$stderr.print "(a|c) 英単語 日本語訳\nd 英単語\np 正規表現\nq\n"
end
end
end
end
readlineライブラリをrequireし、一行入力を可能にします。
このように非常に簡単ですが、Readline.readline
の入力部分はテストする際にはスタブに置き換えて人為的に入力を作り出します。
なお、ここではモックを使うのが難しいのです。
というのは、モックはオブジェクトなのでReadlineに代入したいのですが、Readlineが定数なので再代入できないのです。
それで、モックを直接使うことはできません。
それでは、スタブを使ったテストプログラムを見ていきましょう。
require 'minitest/autorun'
require_relative 'input.rb'
class TestInput < Minitest::Test
def test_input
@in = Input.new
Readline.stub(:readline, "a append 付け足す") { assert_equal(['a', 'append', '付け足す'], @in.input) }
Readline.stub(:readline, "d append") { assert_equal(['d', 'append'], @in.input) }
Readline.stub(:readline, "c append 付け足す") { assert_equal(['c', 'append', '付け足す'], @in.input) }
Readline.stub(:readline, "p a..end") { assert_equal(['p', 'a..end'], @in.input) }
Readline.stub(:readline, "q") { assert_equal(['q'], @in.input) }
m = Minitest::Mock.new
m.expect(:call, "abcd", ["wb > ", false])
m.expect(:call, "q", ["wb > ", false])
Readline.stub(:readline, m) {assert_output(nil, "(a|c) 英単語 日本語訳\nd 英単語\np 正規表現\nq\n"){ @result = @in.input }}
assert_equal(['q'], @result)
end
end
input.rbをrequire_relativeで取り込んでおきます。 test_inputメソッドがテストプログラムです。
{}
の中)で@in.inputでinputメソッドを呼び出す。
Readline.readlineの返した文字列は有効な入力なので、それを配列にして['a', 'append', '付け足す']
を返すはずである。
それをassert_equalでテストする。ここで、前の方に出てきたモックとスタブの組み合わせが使われています。 複雑なので、もう一度説明しましょう。
スタブの引数は、メソッド名、返り値になっています。
返り値には、Procオブジェクトなどを入れることができます。
返り値にはProcオブジェクトが返されるのではなく、Procオブジェクトのcallメソッドを実行した値が返されます。
また、このオブジェクトはProcオブジェクトでなくてもcall
メソッドを持っていれば、同様にcallメソッドの実行結果を返してくれます。
そこで、モックのexpectメソッドでみせかけのメソッドcallを定義します。
すると、stubはモックのcallメソッドを呼び、expectで設定した返り値が返されます。
モックは複数回expectメソッドを使って、順に異なる返り値を設定できます。
ここではモックを使って2回の呼び出しに対して異なる返り値を作成しました。 同じことはProcオブジェクトを使ってもできますし、むしろモックよりも複雑なことをできます。 モックで機能が足りないと思ったらProcオブジェクトを考えてみてください。
テストの実行結果は掲載しませんが、きちんとパスします。
スタブを使うのは複雑になりがちです。 それに対して、前のセクションでクラスのスタブを使ったように、Readlineのスタブを作る方法もあります。 これは、Readlineモジュールのreadlineメソッドをテスト用に再定義してしまう方法です。 こんなおそろしいことをして良いのかと思うかもしれませんが、Rubyでは珍しいことではありません。
ただし、ひとつだけ注意があります。 その再定義が他のテストに影響するかどうか(他のテストでReadline::readlineを使っていないかどうか)を確認してください。 影響がある場合はこの方法を避けるほうが安全です。 というのは、この方法を使い、かつ他のテストへの影響を避けるような対策が難しいからです。 なぜなら、それぞれのテストは並行して実行され、上から順番というわけではありません。 上から順番なら、いったん書き換えたものを戻せば良いのですが、並行実行ではそれが上手く行かないのです。 その点ではstubメソッドを使うメソッド書き換えの方法は安全性が高いです。
require 'minitest/autorun'
require 'readline'
require 'stringio'
require_relative 'input.rb'
module Readline
def self.readline(pronpt="> ", history=false)
unless @stringio
@stringio = StringIO.new("a append 付け足す\nd append\nc append 付け足す\np a..end\nq\nabcd\nq\n")
end
@stringio.readline.chomp
end
end
class TestInput < Minitest::Test
def test_input
@in = Input.new
assert_equal(['a', 'append', '付け足す'], @in.input)
assert_equal(['d', 'append'], @in.input)
assert_equal(['c', 'append', '付け足す'], @in.input)
assert_equal(['p', 'a..end'], @in.input)
assert_equal(['q'], @in.input)
assert_output(nil, "(a|c) 英単語 日本語訳\nd 英単語\np 正規表現\nq\n"){ @result = @in.input }
assert_equal(['q'], @result)
end
end
Readlineモジュールの書き換えのためにrequire 'readline'
が必要です。
readlineはReadlineの特異メソッドなので、def self.readline
として再定義します。
文字列をファイルのように見立てるStringIOというクラスがあります。
このクラスにはreadlineメソッドがあり、文字列から1行ずつ返してくれます。
これがちょうどReadline.readline
の代わりに良いので、再定義の中で使います。
StringIOを使うにはrequire 'stringio'
が必要です(ただ、このプログラムではminitestがrequireしているので、書かなくてもrequireされますが)。
はじめて呼ばれるときは@stringioが未定義なので、StringIOのインスタンスを代入します。
StringIO.new
の引数が入力の元となる文字列です。
2度目の呼び出しではunlessのところを飛び越します。
@stringio.readline
によって、文字列から1行ずつ(つまり\n
で区切られた文字列がひとつずつ)返されます。
Readline.readlineでは行末の改行が切られているので、chompメソッドで改行を落としておきます。
テスト本体ではassert_equalなどで順にInput#inputメソッド(Inputクラスのインスタンスメソッドinputをこのように書くことがあります。 これはドキュメントの中だけで、プログラム中で書くのではありません)をテストするだけです。 このプログラムではstubメソッドを使わずにReadline.readlineを書き換えています。 どちらが良いかは一概に言えませんが、今回のテストプログラムでは後者の方が分かりやすくすっきりとしています。
今回、非常に簡単なプログラムに対して難しいテストプログラムを書きましたが、これは正しい方法なのでしょうか? 私だったら、直接動かしてチェック(人手でチェック)します。 このような簡単で短いプログラムでは、その方が手っ取り早いからです。 今回はテストプログラムを書いたのは、あくまでスタブの説明のためです。
ただ、一般にはテストプログラムは必要で有効なことが多いです。
CSVクラスはcsv(comma separated values)、コンマ区切りデータ形式を扱うクラスです。 IOクラスのように使え、かつコンマ区切りデータを扱えます。 コンマ区切りデータとはその名の通り、行の中でコンマで区切られたデータです。
pen,ペン
bread,パン
このように、各行には同じ数のコンマ区切りのデータがあります。 上記の例はRubyのデータ構造では次のようになります。
[["pen","ペン"], ["bread","パン"]]
<<
演算子を使う次のプログラムは、CSVを使った読み書きの典型的な例です。
# 読み込み
array = CSV.read(CSVファイル名, headers: false)
# 書き出し
CSV.open(CSVファイル名) do |csv|
array.each {|a| csv << a}
end
DBクラスでは単語帳のデータを2次元配列で表し、作業の開始、終了時点でCSVファイルに読み込み、書き出しをします。
db.rbの内部ではデータを2次元配列インスタンス変数@dbに格納し、各メソッドで@dbにデータの付加、削除、変更、照会などをします。 プログラムは短く簡単です。
require "csv"
class DB
def initialize(file='db.csv')
@file = file
if File.exist?(@file)
@db = CSV.read(@file, headers: false)
else
@db = []
end
end
def append(e,j)
@db << [e,j]
end
def delete(e)
i = @db.find_index{|d| e == d[0]}
@db.delete_at(i) if i # i is nil if the search above didn't find e in @db.
end
def change(e,j)
i = @db.find_index{|d| e == d[0]}
if i
@db[i] = [e,j]
else
@db << [e,j]
end
end
def list(e)
pat = Regexp.compile(e)
@db.select{|d| pat =~ d[0]}
end
def close
CSV.open(@file, "wb") do |csv|
@db.each {|x| csv << x}
end
end
end
このプログラムのテストは、2つに分かれます。
本来のテストは1番めだけで良いと思いますが、ここでは2番めもテストします。
require 'minitest/autorun'
require_relative 'db.rb'
class TestDB < Minitest::Test
def test_db
File.stub(:exist?, true) do
CSV.stub(:read, [["pen","ペン"],["pencil","鉛筆"]]) do
@db = DB.new
end
end
assert_equal([["pen","ペン"]], @db.list("^pen$"))
assert_equal([["pen","ペン"],["pencil","鉛筆"]], @db.list("pen"))
@db.append("circle","円")
assert_equal([["circle","円"]], @db.list("cir"))
@db.change("circle","円周")
assert_equal([["circle","円周"]], @db.list("cir"))
@db.delete("pen")
assert_equal([["pencil","鉛筆"], ["circle","円周"]], @db.list("."))
end
def test_csv
File.write("test.csv",<<~CSV)
pen,ペン
pencil,鉛筆
CSV
@db = DB.new("test.csv")
@db.append("circle","円")
@db.change("circle","円周")
@db.delete("pen")
@db.close
assert_equal("pencil,鉛筆\ncircle,円周\n",File.read("test.csv"))
File.delete("test.csv")
end
end
[["pen","ペン"],["pencil","鉛筆"]]
になるとしている実際にテストを実行してみると、すべてパスします。
テストはすべて通ったので、wordtest.rbを実行してみました。 いくつか英単語と日本語訳を入力して、作成されたCSVファイルを見てみると、正しく反映されていました。 小さいプログラムですが、動くと嬉しいものです。 プログラムの今後の発展方向としては
などが考えられます。 ただ単語帳ソフトが本当に役立つプログラムなのかは疑問が残ります。 どうでしょうか? この問に対する答えは英語教育の専門家でなければ出せないでしょう。 一般に、プログラムが有用かどうかは開発者には分からないことが多いです。 その分野の専門家とソフト開発者の協力はとても大切なことです。
今回は実用には程遠い単語帳プログラムではありますが、開発とテストの実例として見てきました。 実際の開発はもっと規模が大きいですが、同様の手順、すなわちユニットごとに作成とテストを繰り返すことになります。 そのときには、minitestを有効に活用して開発を進めてください。
最後にminitestについて述べます。
minitestは高速です。 大きな開発で使うとそれがよく分かります。 なぜかというと複数のテストをマルチメソッドで並行して行うからです。 逆にこのことはテスト相互が独立していないとコンフリクトを起こす可能性があることを示唆しています。 プログラムの上から下へテストするのではなく、各メソッドは同時並行で非同期に進みます。
minitestはウェブ開発フレームワークのRuby on Railsにおける標準のテストシステムになっています。 Railsでは、railsに合うようにminitestの機能を拡張しています。 詳しくはRails Guideを参照してください。 日本語訳もあります。
大きなプログラムのテストでは、Rakeを使ってテストを自動化することができます。 これについては、「はじめてのRake」に説明があります。
今回のテストをするためのRakefileは
require "rake/testtask"
FileList['test*.rb'].each do |file|
Rake::TestTask.new do |t|
t.test_files = [file]
t.verbose = false
end
end
です。 コマンドラインから
rake test
とすると、すべてのテストが実行されます。 rakeに引数testが必要なことに注意してください(通常は引数なしでrakeを起動することが多いので)。
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ
StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例
lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて
文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。
Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。
Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。
今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。
Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...
今回はGTK 3とGTK 4をRubyで使うライブラリについて書きたいと思います。
今回もRubyとGUIのトピックです。 Glimmerを取り上げます。
Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。
Rails7におけるシステムテストについて書きます。
前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。
今回はRailsの慣例に沿った形でWordbookを作り直します。
今回はWordBookの検索と削除についてです。
今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。
一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。
Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...
Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。
minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...
今回もminitestの話です。 mockとstubに焦点をあて説明します。
アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。
今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。
今回は特異メソッド、特異クラス定義、名前空間、モジュール関数について説明します。
2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。
if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。
Procオブジェクトを生成するメソッドlambdaについて説明します。
今回はブロックを一般化したオブジェクトProcを説明します。
ブロック付きメソッドの作り方を説明します。
モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。
クラスの親子関係
Rubyの演算子とその再定義について書きます。
今回からクラスとインスタンスを定義、生成する方法を説明します
Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。
ここでは私が便利だと思ったメソッドを紹介します。
実数
今回はシンボルとハッシュについて説明します。
文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。
配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。
今回の目標はインスタンスです。 インスタンスを説明するために、ローカル変数と文字列オブジェクトを事前に扱います。
今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。
ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。
ここではRubyの最も基本的なオブジェクトである整数について説明します。
「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。