Edited at

Minitest でテスト、Rails のテスト (その1)

More than 3 years have passed since last update.

だいたいぼくは Rails のプロジェクトではテストフレームワークとして Minitest を使っているのですが、だいたい「RSpec しか書いたことなくて Minitest わかんねー」という苦情をいただくので、なんとなく Minitest について、なんか使い方だとか、書き方のコツだとか、なんかそんなようなやつをまとめていこうと思います。

内容は「Minitest Cookbook」の「Rails Recipes」の章を思いっきり参考にさせてもらっています。なので Minitest 自体の説明は少なくて Rails のテストに特化した内容となっています。この本はほとんど唯一の Minitest 本だと思うし、ここで紹介する Rails 絡み以外の部分も良い内容なので読んでおくといいでしょう (そこそこ高いけど) 。

ちなみにこれは「その1」で、まだまだ続く予定ですがいつ書くかは未定です。


Rails プロジェクトでの Minitest のセットアップ


  • assert スタイルでテストを書くのであれば、ActiveSupport があれば他は必要ない

  • spec スタイルでテストを書きたいときは minitest-rails を使おう

  • Rails 5 では rails test コマンドでテストを実行しよう

  • それ以前のバージョンの Rails では Rake タスクでテストを実行しよう


assert スタイルでテストを書くのであれば、ActiveSupport があれば他は必要ない

assert スタイルのテストというのはこういうやつです。

class FizzBuzzTest < ActiveSupport::TestCase

test 'it converts multiples of fifteen to fizzbuzz' do
fizz_buzz = FizzBuzz.new
assert_equal 'FizzBuzz', fizz_buzz.convert(15)
assert_equal 'FizzBuzz', fizz_buzz.convert(30)
end
end

この形式でテストを書く場合、ActiveSupport が入っていれば (というか Rails である限り必ず入っていますが) 、特に追加で何かをインストールしたりする必要はありません。

ただ、

test 'it converts multiples of fifteen to fizzbuzz' do

...
end

という書き方は ActiveSupport (ActiveSupport::TestCase) によって拡張された DSL であり、素の Minitest (Minitest::Test) の記法とは異なることを (一応) 覚えておきましょう。


spec スタイルでテストを書きたいときは minitest-rails を使おう

spec スタイルのテストというのはこういう RSpec-like なやつです。

describe FizzBuzz do

it 'converts multiples of fifteen to fizzbuzz' do
fizz_buzz = FizzBuzz.new
expect(fizz_buzz.convert(15)).must_equal 'FizzBuzz'
expect(fizz_buzz.convert(30)).must_equal 'FizzBuzz'
end
end

minitest-rails を使う場合は test_helper.rb に以下を追加します。

require 'minitest/rails'

Genarator が生成するテストコードも spec スタイルにして欲しければ、config/application.rb に以下を追加します。

config.generators do |g|

g.test_framework :minitest, spec: true
end

どちらのスタイルを採用するかは好みの問題だと思いますが、spec スタイルを使うと以下が使えるようになります。



  • describe ブロックのネスト。基本フラットな assert スタイルと比較してある程度構造化しやすくなる


  • let, subject ブロックの利用。ブロックは遅延評価される。

また、assert スタイルとはだいぶ書き方が違いますが、実際には spec スタイルで使われる Minitest::Spec というクラスは Minitest::Test のサブクラスです。つまり、assert スタイルで使えるものは spec スタイルでも利用可能になっています。例えば、spec スタイルでも assert でアサーションを行うことも可能です。


Rails 5 では rails test コマンドでテストを実行しよう

Rails 5 では rails test で実行する新しいテストランナーが追加されました。テストする対象の選択もしやすいし出力も見やすいので、Rails 5 で Minitest を実行するときはこれを使いましょう。

# test/ 以下のすべてのテストを実行する

$ bin/rails test

# test/models 以下のすべてのテストを実行する
$ bin/rails test test/models

# test/models と test/jobs 以下のすべてのテストを実行する
$ bin/rails test test/models test/jobs

# 特定のファイルの特定の行のテストを実行する
$ bin/rails test test/models/user_test.rb:14


それ以前のバージョンの Rails では Rake タスクでテストを実行しよう

これは省略。


テストデータの管理


  • 永続化が必要ない場合は Object.new でインスタンスを作る

  • パフォーマンスの高さやデータセットの一貫性などの長所を持つ fixtures を使おう

  • fixtures には「機能がわかりやすい」名前や「覚えやすい」名前を付けよう

  • fixtures の構成をどうするかには気を配ろう


    • 最低限の valid な構成を1つ

    • 実際のモデルに近いものを1つ2つ

    • その他、オブジェクトの状態を正しく定義したもの



  • fixtures の定義には ERB や YAML の機能を活用できる


永続化が必要ない場合は Object.new でインスタンスを作る

データ同士の関連性をわかりやすく保つことはテストデータの管理において頭を悩ませる点の1つです。ここでは、それを解消するための方法として、Object.new を利用したシンプルな方針を提案します。

1. 永続化を必要としないテストの場合

シンプルに Object.new を使ってインスタンスを生成しテストする。

describe Product do

it 'knows its price including tax' do
product = Product.new
product.price = 100
assert_equal 108, product.price_with_tax
end
end

2. 関連するオブジェクト (関連モデル、ラッパーなど) に依存するテストの場合

関連オブジェクトに fixtures を使って Object.new でインスタンスを生成し、関連するオブジェクトをベースにテストする。

describe Payment do

it 'has same name as the product' do
payment = Payment.new(product: products(:black_leather_boots))
assert_equal products(:black_leather_boots).name, payment.name
end
end

このように、オブジェクトの関連性を (fixtures 上でではなく) テストそのもので定義することで、テストデータを管理しやすく readability を高めることが可能です。


パフォーマンスの高さやデータセットの一貫性などの長所を持つ fixtures を使おう

fixtures は


  • バリデーションをスキップするので invalid なレコードが挿入されるリスクがある

  • 大規模になるとフラットなファイルで関連モデルのデータの管理をするのはつらすぎる

  • テストロジックとテストデータがわかれているのは readability 悪すぎる

などの理由でディスられて factory_girl や Fabrication が選択されることが多いですが、fixtures ならではの強みもあります。それは、


  • テストの実行前に一度にバルクで読み込まれるため、多くの場合は factory より高速である

  • 常に一貫性のあるデータセットに対してテストを実行できる

  • 一貫性のあるデータじゃないと、finder メソッドや scope のテストをするのは難しい

などです。

テストを実行する際、まず Rails はテストのデータベースを削除し、test/fixtures にある fixtures をすべて読み込みます。そして、それぞれのテストはトランザクション内で実行され、テストの実行後は rollback されて実行前の状態に戻ります。これによって、前に実行されたテストがどんなものであれ、常にクリーンな状態の fixtures データに対してテストをすることができるようになっています。

また、昔のバージョンの fixtures では、アソシエーションを指定するために外部キーを使わないといけなくてダルかったのですが、今はラベルを使ってわかりやすく関連付けを行うことができます。

# payments.yml

black_leather_boots:
amount: 50_000
product: black_leather_boots

blue_suede_shoes:
amount: 40_000
product: blue_suede_shoes


fixtures には「機能がわかりやすい」名前や「覚えやすい」名前を付けよう

前述の fixtures のつらみを軽減するために、fixtures には「機能がわかりやすい」名前や「覚えやすい」名前を付けるようにしましょう。

「覚えやすい」名前とは?

覚えるのが簡単で思い出すのも簡単な名前、更に楽しい感じであればなおいいでしょう。例えば、


  • 漫画のキャラ

  • 映画の登場人物

  • 有名な都市の名前

などです。

「機能がわかりやすい」名前とは?

モデルの状態が一目で明確に把握できる名前です。説明的なものになるので、長くなってしまうこともあるでしょう。例えば、


  • complete

  • valid

  • cannot_be_added_to_order

といった感じです。

しかし、どういった名前が適切なのかはケースバイケースです。

test "completed tasks can't be completed again" do

completed_task1 = tasks(:completed)
completed_task2 = tasks(:freds_task)
...
end

もし上記のテストの場合であれば、tasks(:completed) の方がより意図が明確なので意義のある名前と言えます。

一方で、もしモデルの状態が重要でない場合や、モデル間の関連が重要な場合には、「覚えやすい」名前を付けるように心がけるとよいです。「覚えやすい」名前の方がわかりやすい関連付けを行いやすいことが多いです。


fixtures の構成をどうするかには気を配ろう

アプリケーションが大きくなってくるとテストデータもどんどん大量になってきます。その際にカオスってしまうのを少しでも避けるために、まず、それぞれのモデルに対して、大事な状態をカバーするための fixtures の基本構成を用意するようにしましょう。

最初に以下のセットを用意します。


  • 最低限の valid な attibutes を持ったレコードを1つ

  • 実際に使われるモデルと同じように attibutes をフル装備した現実的なレコードを1つ以上 (たぶんいくつか要る)

  • ある特定の状態や境界値を持ったレコードを必要なだけ

1つ目のレコードはモデルのバリデーションのテストに利用します。まずはこの最低限な状態で valid かどうかテストするようにしてください。そうすることで、後から必須のフィールドやバリデーションの条件を追加したときにテストがコケて教えてくれます。

それから、各バリデーションに対する独立したテストを追加してください (実際には、あまりに単純なバリデーションに対してテストを行う必要はなく、複雑なバリデーションのみのテストで OK だと思いますが) 。

class UserTest < ActiveSupport::TestCase

setup do
@user = users(:barebones) # 最低限の valid レコード
end

test 'barebones user must be valid' do
assert @user.valid?
end

test 'must have email address' do
@user.email = nil
assert @user.invalid?
assert_includes @user.errors[:email], "can't be blank"
end
end

ビジネスロジックのテストを行うとき、このように現実的なレコードだけでなく最低限のレコードも利用することには大きな意味があります。現実的なレコードが想定通りのシナリオでの動作を担保するものである一方で、最低限のレコードに対するテストはロジックをより堅牢にするために有効です。

状態に依存する振る舞いをテストする際には以下のようなエッジケースをテストするようにしましょう。


  • すでに完了した (completed) のタスクを再度完了させてみる

  • 境界値のテストや在庫 0 の商品の購入など


fixtures の定義には ERB や YAML の機能を活用できる

fixtures は YAML で記述されますが、YAML として解釈される前に ERB のインタプリタを通って処理されます。したがって、Ruby コードを埋め込むことで動的な値を利用することが可能です。

morrissey:

email: morrissey@example.com
name: 澄酢 杜夫
company_name: ラフトレード株式会社
created_at: <%= Time.zone.now - 1.day %>
updated_at: <%= Time.zone.now - 1.day %>

ただ、これを利用して大量のデータを動的生成する以下のような fixtures がよくブログなんかで書かれていますが、このようにちょろっとしか違わない大量データ を fixtures を使って生成するのはあまり好ましくありません。ユニットテストは負荷テストをするところではないし、実際の運用で大量のデータを扱うからといって、こうやって作ったデータは結局現実のデータとは別物だからです。

<% 1000.times do |i| %>

user_<%= i %>:
name: user_<%= i %>
<% end %>

database.yml でおなじみの、YAML のエイリアスを fixtures で使うこともできます。

barebones: &minimal

email: nobody@example.com
name: 名無しさん
payment_type: credit_card
created_at: <%= Time.zone.now - 1.day %>
updated_at: <%= Time.zone.now - 1.day %>

morrissey:
<<: *minimal
email: morrissey@example.com
name: 澄酢 杜夫

marr:
<<: *minimal
email: marr@example.com
name: マーくん
payment_type: invoice