1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

コードの信頼を高めよう! RSpecによるRailsテスト入門

Last updated at Posted at 2023-08-26

RSpecとは

 Rubyプログラムやアプリケーションを自動的にテストするための振る舞い駆動型開発(BDD)フレームワーク。

BDDとは

 開発の前に仕様や要件を定義し、それを基にテストを開発者以外がみても理解できるように自然言語に近い形式で記述する。

テストを書く意味

  • アプリケーションの品質担保
  • システム要件の理解度向上
  • プログラムのバグ削減
  • 不具合再発防止
  • 後々リファクタリングしやすくするため。
    「テストコードがない状態だとリファクタリングできない。きれいなコードを書くなら、まずテストコードから」

テストを書くときに意識すること

  • DRYであること(Don't Repeat Yourself:同じことを繰り返すな)
  • 信頼できるものであること
  • 誰が見ても理解できること。将来の人のためにも。
  • テストコードのコーディングルールを厳しくしすぎない。

モデルスペックテスト

validationテスト

RSpec.describe User, type: model do
  describe 'validation' do
#正常系
    it "is valid when firstname, lastname, email, and password are present" do 
      user = User.new(
        firstname: 'John',
        lastname: 'Williams',
        email: 'test@example.com',
        password: 'D3keL1fS'
      )
      expect(user).to be_valid
    end
#異常系
    it "is invalid when firstname is missing" do 
      user = User.new(
        firstname: nil,
        lastname: "Williams",
        email: "test@example.com",
        password: 'D3keL1fS'
      )
      user.valid?
      expect(user.errors[:firstname]).to include("can't be blank")
    end
  end
end
  •  describeで「この機能のテストをするよ。」とテストの構造をグルーピングしている。このケースではvalidationをチェックするもの。
  •   itはテストをexample( it で始まる1行)単位にまとめている。itの後には期待する内容を記述する。このケースでは"firstname, lastnaem, email, passwordがあるときfirstnameがないときの2つのテストを記述。
  •  expectは期待する結果。
    • 正常系はexpect(contact).to be_valid userbe_valid の時。つまりvalid(値が存在している)を期待する。
    •  異常系はvalid?でfalseだったらエラーメッセージが"can't be blank"という文字列を含んでいることを期待する。

 以下の設定がされている場合、firstnameが空白(nilや空の文字列)であると"can't be blank"というエラーメッセージがuser.errors[:firstname]に追加される。

models/user.rb
class User < ApplicationRecord
  validates :firstname, presence: true
  # 他のバリデーションや設定
end

上のテストコードをリファクタリングする

beforeで共通化

 DRY原則にしたがってit内で同じコードがあるので共通化をする。重複しているUseオブジェクト生成を一つにまとめてこれをインスタンス変数として定義する。

user = User.new(
        firstname: "John",
        lastname: "Williams"
        email: "test@example.com",
        password: 'D3keL1fS'
    )

contextでまとめる

 各ブロックで共通している処理があればbeforeで書くのが推奨される。機能をグループ化する点ではdescribe contextも同じだが、contextは条件を分けるときにつかう。より整理されコードが見やすくなる。

RSpec.describe User, type: :model do
  describe 'validation' do
    before :each do
      @user = User.new(
        firstname: "John",
        lastname: "Williams",
        email: "test@example.com",
        password: 'D3keL1fS'
    )

    context 'when firstname, lastname, email, and password are present' do
      it 'is valid' do
        expect(@user).to be_valid
      end
    end

    context 'when firstname is missing' do
      it 'is invalid' do
        @user.firstname = nil
        @user.valid?
        expect(@user.errors[:firstname]).to include("can't be blank")
      end
    end
  end
end

 describeは親、contextは子。つまり親の中でbeforeで定義した@userは子のcontext内で使える。

beforeとlet

 before ブロックを使うと describecontext ブロックの内部で、各テストの実行前に共通のインスタンス変数をセットアップできる。しかし問題点が二つある。

1. before の中に書いたコードは describe や context の内部に書いたテストを実行するたびに 毎回 実行される。これはテストに予期しない影響を及ぼす恐れがある。使う必要のないデータを作成してテストを遅くする原因となる。
2.要件が増えるにつれてテストの可読性を悪くする。

 こうした問題に対処するため、RSpecは let というメソッドを提供している。letは遅延評価であり、必要になった時に使われる。

RSpec.describe User, type: :model do
  describe 'validation' do
    let(:user) do
      User.new(
        firstname: "John",
        lastname: "Williams",
        email: "test@example.com",
        password: 'D3keL1fS'
      )
     end

     context 'when firstname, lastname, email, and password are present' do
       it 'is valid' do
         expect(user).to be_valid
       end
     end

     context 'when firstname is missing' do
       it 'is invalid' do
        user.firstname = nil
        user.valid?
        expect(user.errors[:firstname]).to include("can't be blank")
       end
     end
  end
end

 beforeよりlet の方が可読性がよくなる。

Factory Botでテストデータを作成

 RSpecと組み合わせて使われることが多いテストデータの生成ライブラリ。Factory Botを使用することで、テストケースごとに必要なデータを「工場(Factory)」と呼ばれるテンプレートから簡単に生成できる。

ファクトリの定義

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    firstname { "John" }
    lastname { "Williams" }
    email { "test@example.com" }
    password { 'D3keL1fS' }
  end
end

テスト内での使用

  FactoryBot.build(:user)と書くだけで、簡単にインスタンス化できる。テストデータを作れば、そのユーザーの名前は毎回基本的に John Williams になる。メールアドレスもパスワードも最初から設定された状態になる。

spec/models/user_spec.rb
RSpec.describe User, type: :model do
  let(:user) { FactoryBot.build(:user) }

  it "is valid with a firstname, lastname, email, and password " do 
    expect(user).to be_valid
  end
  it "is not valid without a firstname" do 
    user.firstname = nil
    user.valid?
    expect(user.errors[:firstname]).to include("can't be blank")
  end
end

validationテストはするべきか

 必ずしもこのような単純なvalidationの時はテストの量が膨大になるので書く必要はない。

validates :firstname, presence: true

 しかし、複雑な正規表現、カスタムバリデータ、ややこしい条件、お金が発生する重要な場面ではテストは必要だと考える。以下はemailのフォーマットをチェックしている。

validates :email, presence: true, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i/ }

System Spec

Capybaraでシュミレーションテスト

 ページの読み込み、フォームの入力・送信、リンクやボタンのクリックなど アプリケーションを実際に動かしているかのように動作テストができる。
 Capybara DSLのドキュメントにはそれ以外にも便利なメソッドがある。

メソッド 操作
visit 指定したページに遷移
click_link 指定したリンク文字列を持つaタグをクリック
fill_in 入力したい文字列を指定のフォームに入力
click_button 指定したラベルを持つボタンをクリック

ログインし、プロジェクト作成するテスト

 require 'rails_helper'

RSpec.describe "Projects", type: :system do
  before do
    driven_by(:rack_test)
  end

  scenario "user creates a new project" do  #itと同じexampleの起点
   #最初に新しいテストユーザーを作成
    user = FactoryBot.create(:user)

    visit root_path
    #ログイン画面から作成したユーザーでログイン
    click_link "Sign in"
    #フォームにemailとpasswordを入力
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    #ログインボタンをクリックしてログイン
    click_button "Log in"

    #ユーザーは新しいプロジェクトを作成する
    expect {
      #新しくプロジェクト作成する
      click_link "New Project"
      #フォームにプロジェクト名と説明文を入力
      fill_in "Name", with: "Test Project"
      fill_in "Description", with: "Trying out Capybara"
      click_button "Create Project"
      expect(page).to have_content "Project was successfully created"
      expect(page).to have_content "Test Project"
      #プロジェクトのオーナー名を作成したユーザー名と保有するプロジェクトが+1したものを表示されているか確認
      expect(page).to have_content "Owner: #{user.name}"
    }.to change(user.projects, :count).by(1)
  end
  •  expect{} の中ではブラウザ上でテストしたいステップを明示的に記述し、それから、結果の表示が期待どおりになっていることを検証している。
  •  have_contentで特定のテキストがページ上に表示されているか確認できる。

テスト駆動開発(TDD)。レッドからグリーンへ

 10章までで基礎知識を勉強したあと、11章でテスト駆動開発を行う。スペックを実行してエラー文を確認し、コードを書き加えて赤(エラー状態)」から「緑(成功状態)」に遷移するプロセスを体験できる。

参考書籍

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?