minitest(1)テストとは

アプリ作成の記事で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メソッドで整数にして返す、というメソッドです。 おそらく、皆さんはすぐにこのプログラムの欠点を見つけたことでしょう。 問題ありありのプログラムですが、テストの説明には適しています。

テストプログラムを考える上でのポイントは「メソッドの振る舞い」をテストするということです。 この場合、「引数が与えられたら、その引数の表す数値を返す」というのが振る舞いですが、もう少し詳しく考えないといけません。 引数と返し値の組み合わせを「引数=>返し値」で表すことにします。

  • 数字を表す文字列=>対応する数(ただし、整数と浮動小数点数の2種類の数がある。ここでは複素数、有理想は想定しない)
  • 数字ではない文字列=>??
  • 文字列以外のオブジェクト=>??

??と書いたところは仕様が決められていなかったので、そう書きました。 テストを考えただけで、仕様の不十分さも分かりました。 これもテストを導入するメリットの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は引数について場合分けして、それぞれに対して適切な値を返さなければなりません。

  • 整数を表す文字列=>その整数を返す
  • 浮動小数点数を表す文字列=>その浮動小数点数を返す
  • それ以外の文字列=>nilを返す
  • 文字列でないオブジェクト=>nilを返す

メソッド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番目のシンボルです。

  • :on_int =>整数
  • :on_sp =>空白(タブも:on_spになる)
  • :on_op =>演算子(+や*なども:on_opになる)
  • :on_float =>浮動小数点数

これを利用すると、正規表現を使わずに「整数」「浮動小数点数」「その他」に文字列を分類できます。 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]のように、数字の文字列(to_numberの引数)と数字(to_numberが返すオブジェクト)となっている。 最初の例では”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のドキュメントに詳細がありますので、参考にして下さい。

2023

単項マイナスと構文解析

1 minute read

単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合

Raccライブラリと構文解析

3 minute read

パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ

StrScanライブラリと字句解析

less than 1 minute read

StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例

Gem

1 minute read

lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて

Encoding

1 minute read

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

Thread

less than 1 minute read

Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。

Fiber

1 minute read

Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。

RDoc

less than 1 minute read

今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。

Back to Top ↑

2022

Ruby/GTK4

5 minute read

Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...

Shoes – Rubyとグラフィック

5 minute read

Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。

Rails7 テスト

5 minute read

前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。

Rails7 モデルとデータベース

2 minute read

今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。

Rails7とBootstrap

2 minute read

一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。

Rails7のインストール

2 minute read

Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...

GemとBundler

1 minute read

Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。

minitest(3)モックの詳細

2 minute read

minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...

minitest(1)テストとは

2 minute read

アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。

public、private、protected

2 minute read

今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。

アプリ制作、インストール、テスト

1 minute read

2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。

case文

2 minute read

if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。

Lambda

2 minute read

Procオブジェクトを生成するメソッドlambdaについて説明します。

Proc オブジェクト

2 minute read

今回はブロックを一般化したオブジェクトProcを説明します。

モジュール

1 minute read

モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。

Kernelモジュール

less than 1 minute read

Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。

便利なメソッド

1 minute read

ここでは私が便利だと思ったメソッドを紹介します。

文字列と正規表現

3 minute read

文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。

配列

2 minute read

配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。

トップレベルのメソッド

1 minute read

今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。

ブロックとイテレータ

less than 1 minute read

ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。

整数

less than 1 minute read

ここではRubyの最も基本的なオブジェクトである整数について説明します。

Hello world

less than 1 minute read

「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。

Back to Top ↑