fv17の日記 - Coding Every Day

Webエンジニアの備忘用ブログです。主にWeb界隈の技術に関して書いています。

RSpec - モデルスペックとは

「Everyday Rails - RSpecによるRailsテスト入門 - 第3章 モデルスペック 」の復習用まとめ。

leanpub.com

本章のゴール

モデルスペックの構造

モデルスペックには次を含める。

  • 有効な属性で初期化された場合は、モデルの状態が有効(valid)になっていること
  • バリデーションを失敗させるデータであれば、モデルの状態が無効(invalid)であること
  • クラスメソッド、インスタンスメソッド、スコープが期待通りに動作すること
  • 正常系テストと異常系テストの両方を書く(例えば、スコープで検索したが結果ゼロ、など)

モデルのテストのベストプラクティス

  1. 期待する結果をまとめてdescribeし、モデルの仕様や振る舞いを示す。
  2. テスト1つ毎に結果を1つだけ期待し、問題箇所を素早く特定できる。
  3. どのexampleも説明文を付け、何をテストしているか明確にする。

モデルスペックを作成する

スペックの作成

bin/rails generate rspec:model user

spec/models/user_spec.rbが作成される

スペックの実行

bin/rspec

アウトラインの作成

複数テストを一つにまとめないのが肝。テストが失敗した場合に、失敗箇所の特定が早いから。

require 'rails_helper'

RSpec.describe User, type: :model do
  # 姓、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a first name, last name, email, and password"
  # 名がなければ無効な状態であること
  it "is invalid without a first name" 
  # 姓がなければ無効な状態であること
  it "is invalid without a last name" 
  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address"
  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address"
  # ユーザーのフルネームを文字列として返すこと
  it "returns a user's full name as a string"
end

Rspecの構文 / バリデーションをテストする

itの後には何をテストしているのかを書く。下記は英語だが、日本語で書いても良い。

# 姓、名、メール、パスワードがあれば有効な状態であること
it "is valid with a first name, last name, email, and password" do
  user = User.new(
    first_name: "Aaron",
    last_name: "Sumner",
    email: "test@exaple.com",
    password: "test-password"
  )
  expect(user).to be_valid
end

# 名がなければ無効な状態であること
it "is invalid without a first name" do
  user = User.new(first_name: nil)
  user.valid?
  expect(user.errors[:first_name]).to include("can't be blank")
end

# rails tutorialではエラーメッセージまではチェックしていない
it "is invalid without a first name(rails tutorial ver.)" do
  user = User.new(first_name: nil)
  expect(user).to_not be_valid
end

テストをわざと失敗させる

3つの方法がある。

  • toをto_notに変更する
  • アプリケーション側のコードを変更する(コメントアウトするなど)
  • テスト側のコードを変更する

テストの内容

  • 正常系のパターン
  • エラーが発生する条件でのパターン
  • 境界値分析などのテスト(6~10文字を要求するならば、5,6,10,11文字でテスト)

マッチャについてもっと詳しく

itojunさんの記事を参照
qiita.com

テストのリファクタリング(DRYにする)

describeとcontextを使い、テストのアウトラインを整理する

describeはクラスやシステムの機能に関するアウトラインを記述
contextは特定の状態に関するアウトラインを記述

# 文字列に一致するメッセージを検索
describe "search message for a term" do
  # 一致したデータが見つかる場合
  context "when a match is found" do
    it "returns notes that match the search term" do
      # 省略
    end
  end
  # 一致したデータが見つからない場合
  context "when a match is found" do
    it "returns an empty collection" do
      # 省略
    end
  end
end

beforeを使い、必要な前処理を一箇所にまとめる

before do
  # 各テストで利用するためにインスタンス変数にすることを忘れずに
  @user = User.create(
    first_name: "Taro",
    last_name: "Tanaka",
    email: "taro@example.com",
    password: "example"
  )
end

各example終了後に後片付けがしたい場合、例えば外部サービスとの接続を切るなど、afterを使うことで処理を共通化することができる。

DRYのしすぎには注意

テストファイルを上下にスクロールしまくっているならば注意。その場合、beforeでまとめていた処理を各describeやcontextに配置し、何をやろうとしているのかが一目でわかるようにして可読性を上げることを検討する。

まとめ

  • 期待する結果は明示的に記述すべし
  • 原則として、期待する結果は1つのexampleに対して1つにすべし
  • 正常系テストだけでなく、異常系テストもすべし
  • 境界値テストをすべし
  • 可読性を上げるために、describe、context、beforeなどを上手く使ってスペックを整理すべし