こんにちは。ルーターの学生アルバイトのkondoです。

作成したプログラムの動作を保証する仕組みとして、テストコードを書くことがあります。

今回は、Rubyでのテストコードの書き方について紹介します。

テストコードとは

書いたコードが正しく動いているかどうかを判定する方法として、テストコードというものがあります。以下のような手順でコードのテストを行います。

  1. テストをしたいメソッドを作る
  2. 引数と返り値のセットを考え、それをテストコードにする
  3. テストコードを実行し、結果を確認する

(このテスト方法は「単体テスト」と呼ばれています。他に「結合テスト」というものもありますが、この記事では触れません)

テストコードを書くのは面倒に思えますが、テストを書くことは以下のようなメリットがあります。

  • メソッドごとにテストをすることができ、不具合が起きたときの原因を見つけやすい
  • 引数と想定する返り値を指定しメソッドを動かす書き方で統一されているので、コードの品質が把握しやすい

ここではRubyのMinitestというgemを用いたテストコードの書き方を紹介します。

通常のテスト

あらかじめMinitestのgemをインストールしておきましょう。bundlerを使っているのであれば、
Gemfileにgem 'minitest'と追加しましょう。

テストコードは、実際に動かすコードとは別のものとして作成します。

ここでは、以下のcounter.rb用のテストを例に説明します。文字列の平仮名の数を数えるメソッドを実装したプログラムです。

  class Counter
    def hiragana_count(str)
      str.gsub(/[^ぁ-ん]/,'').size
    end
  end

テストコードは以下のようになります。

  require 'minitest/autorun'
  require_relative 'counter.rb'
  class TestCounter < Minitest::Unit::TestCase
    def setup
      @counter = Counter.new
    end

    def test_hiregana_count
      str = 'この文章には平仮名が9文字あります。'
      actual = @counter.send(:hiragana_count, str)
      expected = 9
      assert_equal expected, actual
    end
  end

テストコードについて詳しく見てみましょう。

  1. minitestとテストをするコードをrequireし、Minitest::Unit::TestCaseのクラスを継承したテストクラスを作っています。autorunを呼ぶことで、テストコード実行時に自動でテストをしてくれます。
  2. setupメソッドはテスト前に呼ばれるメソッドです。ここでテストをするコードのクラスであるCounterクラスのインスタンスを作ります。
  3. テストをする関数ごとに、テスト用メソッドを作成しています。(def test_hiregana_countの所)
    テストメソッド名は「test_ + テストをするメソッド名」にする必要があります。
  4. 「メソッドに渡す引数」「その引数を渡したときに予想されるメソッドの返り値」を定義してあげます。 この返り値が、実際にコードを動かしたときの返り値と一致すれば、テスト成功です。
  5. setupメソッドで作ったクラスのインスタンスには、sendメソッドが継承されています。このメソッドの引数に、テストをするメソッド名とその引数を渡すと、結果が帰ります。 ここでは結果は変数actualに代入しています。
  6. assert_equalメソッドに、先程の「その引数を渡したときに予想されるメソッドの返り値」と「sendメソッドで返ってきた結果」を渡すと、テストをしてくれます。

テストコードをrubyコマンドで実行することで、テストが行われます。

テストが成功したときはこのような出力となります。

  MiniTest::Unit::TestCase is now Minitest::Test. From test_counter.rb:3:in `'
  Run options: --seed 20672

  # Running:

  .

  Finished in 0.001069s, 935.4537 runs/s, 935.4537 assertions/s.

  1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

failuresとerrorsの値が0なので、テストは成功です。失敗したときは以下のように、どこのメソッドでどのように失敗したかを表示してくれます。

期待する返り値と実際の結果が一致せずに失敗した場合↓(期待する返り値を20にして実行しています)

  MiniTest::Unit::TestCase is now Minitest::Test. From test_counter.rb:3:in `'
  Run options: --seed 63245

  # Running:

  F

  Finished in 0.001892s, 528.5412 runs/s, 528.5412 assertions/s.

    1) Failure:
  TestFormatter#test_hiregana_count [test_counter.rb:12]:
  Expected: 20
    Actual: 9

  1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

  ~/programing/study/study_test/test_counter

エラーが起きて失敗した場合↓(引数にnilを渡してテストをしたので、NoMethodErrorが起きています)

  MiniTest::Unit::TestCase is now Minitest::Test. From test_counter.rb:3:in `'
  Run options: --seed 30229

  # Running:

  E

  Finished in 0.000979s, 1021.4505 runs/s, 0.0000 assertions/s.

    1) Error:
  TestFormatter#test_hiregana_count:
  NoMethodError: undefined method `gsub' for nil:NilClass
      /Users/kondodaichi/programing/study/study_test/test_counter/counter.rb:6:in `hiragana_count'
      test_counter.rb:11:in `test_hiregana_count'

  1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

エラーのテスト

ここからはエラーのテスト方法を紹介します。エラーのテストは、

  • 発生した例外クラスを指定するテスト
  • 発生した例外オブジェクトに対するテスト

のように、様々な形式があります。1つ目の方のテストを紹介します。

test_counter.rbのtest_hiragana_countメソッドを、エラーテスト用に書き足します。assert_raisesメソッドの引数に例外クラスを渡し、ブロック内でsendメソッドを実行させています。

  def test_hiregana_count
    str = 'この文章には平仮名が9文字あります。'
    actual = @counter.send(:hiragana_count, str)
    expected = 9
    assert_equal expected, actual

    str = 123
    assert_raises NoMethodError do
      @counter.send(:hiragana_count, str)
    end
  end

上の例では、引数に123を渡したときにNoMethodErrorが正しく発生することをテストしています。テストの成功失敗は、さきほどの結果と同じように出力されます。

assert_raisesメソッドの返り値は、起きたエラーの例外オブジェクトとなっています。その返り値を利用して、エラーメッセージのテストなども行えます。

おわりに

紹介したMinitestの機能はほんの一部です。Mintestの他に、Rspecなどのテスト用フレームワークもあります。
テストコードの入門として、一度Minitestから触れてみるのはどうでしょうか?