大学生がプログラマーを目指すはなし

ruby初心者の自分が忘れないためのレポート

スはスペックのス(model)をRspec3, Rails5でやってみた。

こちらの「スはスペックのス」なのですが、とても古い記事でして2018年にrubyの勉強をし始めた僕にとってはversionの観点から少し分かりづらい内容でした。さらにネットの情報も様々なversionで書かれた記事が混在しており、さらにやりにくかったです。   

そんな中、頑張ってとりあえず全部やったのでメモとしてブログに上げます。

スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)

スはスペックのス(model編)

環境

  • Rspec → version 3.8
  • ruby → version 2.5.1
  • Rails → version 5.2.0

0. 準備

インストール

RSpec のリリースバージョンは RubyGems で提供されています。

$ gem install rspec

RSpec が無事にインストールされて、実行できることを確認しましょう。

$ spec -v

1. 簡単な配列のテスト

まず最初にRailsを使ってテストをする前に簡単な配列のテストをして、雰囲気を感じましょう。

適当なディレクトリに array_spec.rb という名前のファイルを用意します。 RSpec によるテストコードのファイル名は接尾辞を _spec とする のが一般的です。なお、RSpecで振舞を記述したファイルのことをスペックファイルと呼びます。

スペックファイルには、次のようにスペックを書きます。

array_spec.rb

describe Array, "when empty" do #ここに何を対象としてテストをするのか書く。
  before do #テスト前準備
    @empty_array = []
  end

  it "should be empty" do #ここにテストの内容を書く。
    expect(@empty_array).to be_empty
  end

  it "should size 0" do #ここにテストの内容を書く。 
    expect(@empty_array.size).to eq 0
  end

  after do #テスト後の後片付け
    @empty_array = nil
  end
end

 こちらのテストを読んでいくと、

まず最初にテストの内容です。

describe Array, "when empty" do #ここに何を対象としてテストをするのか書く。

これは"配列が空のとき"と読めます。

次に、 テスト内容です。

it "should be empty" do #ここにテストの内容を書く。
  @empty_array.should be_empty
end

これの"it"の部分から読んでみると、"それは空であるべきだ"と読めます。

次もテスト内容です。

it "should size 0" do #ここにテストの内容を書く。 
  expect(@empty_array.size).to eq 0
end

 これの"it"の部分から読んでみると、"それは0サイズであるべきだ"と読めます。

まとめると、

 - 配列が空のとき、 describe 〜

 - それは空なのか、 it "should be empty" do

 - 配列の数は0なのか、 it "should size 0" do

これらをテストしているということになります。

RSpec を実行してみます。

$ rspec
..
2 examples, 0 failures

 examplesがテストの数で、failuresがそのうち失敗した数です。つまり、全てのテストは成功したということです。 

2. Railsでmodelテストの作成

railsアプリケーションを作ります。

$ rails new rspec_sample_model
 ...(中略)
 $ cd rspec_sample_model

 Blog modelを作成します。

$ rails g model Blog name:string
 exists  ...
 create  ...
 ...
...

 たくさんファイルができます。

マイグレーションファイルを編集する。

ブログに名前が無いと困るので必須項目にしておきましょう。blogs テーブルの name 属性を null 不可にします。

db/migrate/001_create_blogs.rb

class CreateBlogs < ActiveRecord::Migration[5.2]

  def change

    create_table :blogs do |t|

      t.string :name, :null => false # Not null

 

      t.timestamps

    end

  end

end

DBを作成する。

$ bundle exec rake db:migrate

フィクスチャを編集する。

少し先回りになりますが、こちらを編集しておきます。

kubota:
  id: 1
  name: rspec_practice
kobayashi:
  id: 2
  name: css_practice

3. Blogのバリデーションのスペックを定義する。

spec/models/blog_spec.rb

require 'rails_helper'
describe Blog do before(:each) do @blog = Blog.new end

  it 'is not be valid without name' do
    expect(@blog).not_to be_valid
  end
end 

ここで実行すると失敗するはずです。なぜならまだ Blogクラスにname属性を検証するような実装をしていないからです。

一応、実行してみます。

rspec spec/models/blog_spec.rb

 

F

 

Failures:

 

  1) Blog is not be valid without name

     Failure/Error: expect(@blog).not_to be_valid

       expected #<Blog id: nil, name: nil, created_at: nil, updated_at: nil> not to be valid

     # ./spec/models/blog_spec.rb:11:in `block (2 levels) in <top (required)>'

 

Finished in 0.05637 seconds (files took 1.71 seconds to load)

1 examples, 1 failure 

予想通り失敗しました。

Blogクラスに期待される振舞を実装する。

name 属性の存在を検証するように Blog クラスを実装します。

app/models/blog.rb

class Blog < ActiveRecord::Base

  validates_presence_of :name

end

blog.rb の修正を保存して、再度 rspec を実行します。DBに変更は無いので、そのまま rspecコマンドを叩きます。

rspec spec/models/blog_spec.rb

.

 

Finished in 0.05556 seconds (files took 1.71 seconds to load)

3 examples, 0 failures

予想通り成功しました。 

3. Entryモデルの作成。

次は Blog に投稿された記事を表現する Entry モデルを作成します。 ジェネレータの利用からフィクスチャの設定までは Blog クラスの場合と同様です。

Entryモデルを生成する。

Entry の属性にはタイトル、本文、投稿日を用意します。Blog と関連づけるため の外部参照キーや、管理用の属性として、タイムスタンプも定義します。

$ rails g model entry title:string body:text posted_at:date created_at:timestamp updated_at:timestamp blog_id:integer

exists app/models/

exists spec/models/

create spec/fixtures/

create app/models/entry.rb

create spec/fixtures/entries.yml

create spec/models/entry_spec.rb

exists db/migrate

create db/migrate/002_create_entries.rb

マイグレーションファイルを編集する。

 必須属性を null 不可にします。

db/migrate/002_create_entries.rb

class CreateEntries < ActiveRecord::Migration[5.2]

  def change

    create_table :entries do |t|

      t.string :title, :null => false

      t.text :body, :null => false

      t.timestamp :posted_at

      t.timestamp :created_at

      t.timestamp :updated_at

      t.integer :blog_id, :null => false

 

      t.timestamps

    end

  end

end

投稿日 (posted_at) を created_at や updated_at といった Rails が標準でサポート するタイムスタンプで代用せずに独立させて定義しています。

これは「Blog の記事の投稿日」として扱いたい日付が必ずしもデータベースレコードの作成・更新タイムスタンプと同じとは限らないためです。 

DBを移行する。

$ bundle exec rake db:migrate

development環境のデータベースを変更しましょう。

フィクスチャを編集する。

テスト用の投稿記事を用意しておきます。

spec/fixtures/entries.yml

$ bundle exec rake db:migrate

kubota_earliest:

   id: 1

  blog_id: 1

  title: "昨日は台風"

  body: "せっかくエンジニアの人とランチの約束があったのにダメになった。"

  posted_at: 2001-07-06

kubota_latest:

  id: 2

  blog_id: 1

  title: "最近はRspecにハマっている。"

  body: "なんだかかれこれ1週間くらいやっているなぁ。"

  posted_at: 2007-09-12

kobayashi:

  id: 3

  blog_id: 2

  title: "全然休みがない"

  body: "金もない。"

  posted_at: 2007-06-24

4. BlogとEntryを絡み合わせたspecを定義する。

では実装に入ります。今回作成するブログアプリケーションのモデルを確認しておきます。

両者の関連を確認しておくと:

  • Blog は複数の Entry を所有する (Blog has_many Entry)
  • Entry は特定の Blog に属する (Entry belongs_to Blog)

となります。この 2 つをスペックとして記述します。

せっかく Entry モデルを作成したので、まずは Entry 側の関連からスペックを定 義しましょう。

「Entryは特定のBlogに属すること」をスペックとして定義する。

 Entry から Blog を参照できるという期待をスペックとして記述します。

spec/models/entry_spec.rb

require 'rails_helper' 

describe Entry do

  fixtures :entries, :blogs

  before(:each) do

    @entry = entries(:kubota_earliest)

  end

 

  it 'belongs to a specific blog.' do

    expect(@entry.blog).to eq blogs(:kubota)

  end

end

RSpec on Rails では振舞を記述する際に、Rails 標準のテスティング環境と同様 に fixtures メソッドを使用できます。Rspec on Rails の fixtures と Rails 標準の fixtures との違いは、Rspec on Rails では「fixture_path」が 「$RAILS_APP/spec/fixtures」に設定されていることです。

spec を実行する前に、rake の db:test:prepare タスクを実行します。 entries テーブルを作成した際に、development 環境のデータベーススキーマは 変更しましたが、この変更をまだ test 環境のデータベースには反映していないからです。

$ bundle exec rake db:test:prepare

 ここでテストを実行すると失敗するはずです。なぜならまだEntry.blogを定義していません。

一応実行してみます。

$ rspec spec/models/entry_spec.rb

F

 

Failures:

 

  1) Entry belongs to a specific blog.

     Failure/Error: expect(@entry.blog).to eq blogs(:kakutani)

     

     NoMethodError:

       undefined method `blog' for #<Entry:0x00007f868f774828>

       Did you mean?  blog_id

     # ./spec/models/entry_spec.rb:18:in `block (2 levels) in <top (required)>'

 

Finished in 0.03105 seconds (files took 1.71 seconds to load)

1 example, 1 failure

予想通り失敗しました。 

Entryにbelongs_toを実装する。

振舞の期待通りに Entry を実装しましょう。

app/models/entry.rb 

class Entry < ApplicationRecord

  belongs_to :blog

end

spec を実行します。 

$ rspec spec/models/entry_spec.rb

.

 

Finished in 0.04034 seconds (files took 1.7 seconds to load)

1 example, 0 failures

予想通り成功しました。

とりあえずここまで。