アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。
テストとは
一般にテストはプログラムのバグを除くために広範に使われている手法です。 開発に従事している方は既に良くご存知のことと思います。 私は「仕事として」つまり給料をもらって開発をしたことがありません。 おそらくプロの開発の方は、私よりもっとテストの経験も知識もお持ちだと思います。 そういう方は、この記事を読む必要はないかもしれません。
また、テストには「テスト駆動開発」とか「振る舞い駆動開発」などというのもあって、ひとつの開発手法にもなっています。
- テスト駆動開発=>Test Driven Development=>TDD
- 振る舞い駆動開発=>Behavior Driven Development=>BDD
そのような手法では、まずテストを書いてからアプリを書く、といわれます。 ですが、私はどちらが先でも良いのではないかと思っています。
テストを書くときは、あまりプログラムの細かい部分に立ち入るよりも、その振る舞い(behavior)が期待通りかをテストするようにします。 そうすることで、そのプログラムの内部構造を変更、改良したのちも同じテストプログラムでテストすることができます。 つまり、テストは内部構造まで立ち入らず、振る舞いのみにフォーカスすべきです。
また、テストではその規模も問題になります。
- ユニットテスト: クラスあるいはメソッドなどのプログラムを構成する単位に対して行うテスト。 比較的簡単に作成、テストができる
- 統合テスト: ユニットテストの対象よりも大きなプログラムの部分、あるいは全体について行うテスト。 プログラムの構成によってその実際は変わるが、例えばユニットテスト済みの単体同士の連携などをテストする。 このような大きなテストでは入出力、ネットワーク、データベースなどを扱うために、特別な方法が必要になることが多い。
この記事ではユニットテストを扱います。
自作テストプログラム
minitestはテスト用のフレームワークですが、それを使わずに自分でテストプログラムを作ることもできます。
例えば、あるメソッドが数字を表す文字列を引数に取り、その数値を返す、というものだとしましょう。
メソッド名をto_number
としておきましょう。
今回は先にto_number
を作ってから、そのテストプログラムを作ることにします。
def to_number(s) s.to_i end
引数の文字列をto_i
メソッドで整数にして返す、というメソッドです。
おそらく、皆さんはすぐにこのプログラムの欠点を見つけたことでしょう。
問題ありありのプログラムですが、テストの説明には適しています。
テストプログラムを考える上でのポイントは「メソッドの振る舞い」をテストするということです。 この場合、「引数が与えられたら、その引数の表す数値を返す」というのが振る舞いですが、もう少し詳しく考えないといけません。 引数と返し値の組み合わせを「引数=>返し値」で表すことにします。
??と書いたところは仕様が決められていなかったので、そう書きました。 テストを考えただけで、仕様の不十分さも分かりました。 これもテストを導入するメリットの1つです。 つまり、振る舞いに対するテストを書くためには、振る舞いをより完全に定義しなければならなくなります。
今回は??のところはnilを返して、エラー処理は呼び出し側に任せることにしましょう。
テストはこんな感じです。
def to_number(s) s.to_i end # テストプログラム def test_to_number a = to_number("0") print "0 にならずに #{a} が返された\n" unless a == 0 a = to_number("-0.1") print "-0.1にならずに #{a} が返された\n" unless a == -0.1 a = to_number("1.23e10") print "1.23e10にならずに #{a} が返された\n" unless a == 1.23e10 a = to_number("abc") print "nilにならずに #{a} が返された\n" unless a == nil a = to_number(100) print "nilにならずに #{a} が返された\n" unless a == nil end test_to_number #=> -0.1にならずに 0 が返された #=> 1.23e10にならずに 1 が返された #=> nilにならずに 0 が返された #=> nilにならずに 100 が返された
最初のテストだけ通って、残りの4つは通りませんでした。 テストを通ることを「成功(サクセス)」通らないことを「失敗(フェイル)」といいます。 またテストを通ることを「パスする」ともいいます。
to_number
は引数について場合分けして、それぞれに対して適切な値を返さなければなりません。
メソッドto_number
を書き直してみましょう。
def to_number(s) return nil unless s.is_a?(String) case s when /\A-?\d+\Z/ s.to_i when /\A-?\d+\.\d+([Ee]\d+)?\Z/ s.to_f else nil end end
正規表現を使って、文字列が数字にマッチする場合に数値に変換しています。 結構複雑なことをしなければならないのですね。 これでテストは通るようになります。
Ripper
ちょっと脇道にそれますが、このように文字列が数値を表すかどうかをチェックするのは一種の字句解析に当たります。 rubyはプログラムを読む時に「字句解析」をして、その文字列をキーワード、識別子、区切り記号、数値などに分解したのちに構文解析、実行に移ります。 そこで、rubyの字句解析を使って数値を取り出すこともひとつの方法として考えられます。 ripperという標準添付ライブラリはRuby プログラムを解析するためのライブラリです。 メソッドRipper.lexは字句解析をし、字句(トークン)の配列を返します。
require 'ripper' p Ripper.lex("123 -1.2e5") #=> [[[1, 0], :on_int, "123", END], [[1, 3], :on_sp, " ", END], [[1, 4], :on_op, "-", BEG], [[1, 5], :on_float, "1.2e5", END]]
配列の中身の詳細はRubyのドキュメントを参照してください。 注目すべきは、各配列の2番目のシンボルです。
これを利用すると、正規表現を使わずに「整数」「浮動小数点数」「その他」に文字列を分類できます。
to_number
をRipper.lexを使って書き直してみましょう。
require 'ripper' # メソッドの再々定義 def to_number(s) return nil unless s.is_a?(String) a = Ripper.lex(s) unless a.size == 1 || (a.size == 2 && a[0][1] == :on_op && a[0][2] == "-") return nil end case a[-1][1] when :on_int s.to_i when :on_float s.to_f else nil end end
Ripperを使うことはあまりないでしょうが、Ruby同様の字句解析をしたいときはRipperが便利です。 本題とはあまり関係ないことを書きましたが、Ripperも面白いと思っていたので、この機会に紹介しました。
minitest
to_numberメソッドをテストするminitestのプログラムを作ってみましょう。 次のプログラムはto_numberに対して8つのテストをします。 to_numberメソッドはこのプログラムの前に挿入しておくこととします。 (require_relativeメソッドで取り込んでも良いです)。
require 'minitest/autorun' class TestToNumber < Minitest::Test def setup end def teardown end def test_to_number [["0",0],["12",12],["-5",-5],["12.34",12.34],["2.27421e5",2.27421e5],["-23.56",-23.56],["abc",nil], [123,nil]].each do |s,v| if v == nil assert_nil to_number(s) else assert_equal v, to_number(s) end end end end
要点を次に記します。
- minitest/autorunをrequireメソッドで取り込む。 なお、minitestは標準ライブラリなのでインストール不要
- MiniTest::TesTのサブクラスとして、テスト用のクラスを定義する。 クラス内には、setup、teardown、テスト用のメソッドを記述する
- setupはテスト前に行う準備を記述する。 良くあるのは、インスタンスの生成など
- teardownはテスト後の後始末をする。 上記のプログラムではsetupとteardownは何もしていないので、記述しなくても良い
- テストのメソッドはその名前の先頭に
test_
をつける。 ここではto_number
メソッドのテストなのでtest_to_number
とした。 ここでは、2重配列を使い、その8つの要素に対してeachメソッドを用い、テストする。 配列のそれぞれの要素は`["0",0]のように、数字の文字列(=>引数)と数字(返されるオブジェクト)となっている。 最初の例では"0"を引数にしてメソッドを呼び出したとき、0が返されることが期待されている。 期待通りならテストは通過(パス)し、そうでなければ失敗(フェイル)となる。 - assert_nilは実行結果とnilを比較する。 つまり、期待される結果がnilで実行結果がassert_nilの引数になる。
- assert_equalはminitestが提供するテスト用メソッドで、2つの引数をとる。
第1引数には期待されるオブジェクト、第2引数には実際にテストで得られたオブジェクトを書く。
両者が一致すればパス、一致しなければフェイルになる。
なお、期待されるオブジェクトがnilのときは、
assert_equal nil, (テストで得られたオブジェクト)
とすることもできるが、これは古い書き方で現在は推奨されていない。assert_nil
が推奨される書き方になっている。 テスト通過の場合は何も表示されず、失敗のときは期待された(expected)オブジェクトと実際の(actual)オブジェクトが表示される。
実行してみます。
$ ruby example23.rb Run options: --seed 7828 # Running: . Finished in 0.005369s, 186.2654 runs/s, 1490.1234 assertions/s. 1 runs, 8 assertions, 0 failures, 0 errors, 0 skips
テストは成功したので、フェイルの表示が出ません。
ドットはテストするメソッド(この場合はtest_to_number
)の個数分だけ表示されます。
一番下の行の「8 assertions」は8つのassert_・・・
のテスト(アサーションと呼ぶ)が行われ、失敗0エラー0だったことを示しています。
なお、失敗は「期待した値と実行結果の値が異なること」、エラーは「プログラムのエラー(間違い)」で、違うものを表します。
このテストはトップレベルのメソッドを対象としました。
もしこのメソッドがトップレベルで定義されたインスタンス変数を使っているときは、その定義をsetupで行わなければなりません。
(トップレベルで行う定義をそっくりコピペすれば良い)。
トップレベルのインスタンス変数はmain(トップレベルのオブジェクト)のインスタンス変数になります。
これに対して、minitestのテストはMinitest::Test
クラスのサブクラスに定義されたメソッドで行われるので、そこで参照されるインスタンス変数は「そのサブクラスのインスタンスの変数」になります。
したがって、mainのインスタンス変数は参照できないことに注意してください。
これを解決するには、さきほど述べたように、setupメソッドでインスタンス変数を定義しておきます。
標準出力のテスト
次のプログラムはテストするまでもない簡単なものですが、標準出力のテスト例として取り上げます。
@n = 0 def inc @n += 1 p @n end inc inc inc
メソッドincはインスタンス変数@nを1つ大きくし、標準出力に出力します。 トップレベルでincを3回呼ぶと、
1 2 3
と、呼び出すごとに1つずつ大きくなって表示されます。
このテストは次のようになります。
class TestInc < Minitest::Test def setup @n=0 end def teardown end def test_inc assert_output("1\n") {inc} assert_output("2\n") {inc} assert_output("3\n") {inc} end end
大事なことはsetupでインスタンス変数@nの初期化をしなければなりません。
前項で述べたように、トップレベルのインスタンスがmainであるのに対し、test_inc
メソッドのインスタンスはTestIncクラスのインスタンスです。
したがって、トップレベルのインスタンス変数を参照することはできません。
setupでの@nの初期化は必須で、これがないとincの@n += 1
を実行するときに、@nが未定義で参照されるためにnilになり、nilには+メソッドがないのでエラーになります。
これはエラーであってフェイルではありません。
このテストではassert_output
メソッドを使っています。
引数が期待される結果、それをブロックの実行の標準出力と比較して異なるとフェイル・メッセージが表示されます。
自作のテストプログラムで標準出力をテストするには、出力を捕まえる(出力先を変更する)必要があり、大変ですが、minitestを使えばassert_output
を使うだけですみます。
minitestのアサーションは豊富です。 minitestのドキュメントに詳細がありますので、参考にして下さい。