Rspecのことが分からなさすぎて、過去に調べたことを備忘録として残しておこうと思います。
Contents
Rspec
RspecはRuby言語をベースにした「ドメイン特化言語 (Domain Specific Language:DSL)」。ドメイン特化型言語 とは、特定の問題領域 (ドメイン) を記述するために設計された「言語」です。RSpecが特化しているドメインは「開発対象のプログラムの振舞を記述する」という領域。
『TDD』 の進め方と原則
TDD の進め方はいたってシンプルに3つ。
1. プロダクトコードを書く前にテストコードを書き、それが失敗することを確認する (レッド)
2. テストに成功するようにプロダクトコードを書く (グリーン)
3. プログラムの振る舞いを変えないように、プロダクトコードの重複などを整理する (リファクタリング)
TDDの原則もシンプルに3つ。
1. テストに失敗しない限り、プロダクトコードを書いてはいけない。
2. プロダクトコードはテストを通るように書く
3. テストは少しずつ書き進めていく
ソフトウェア
厳密にいえば、ソースコード、すなわちソフトウェア設計は次の2つの要素から構成される。
テストコード: プログラムがどのように振る舞うべきかを定義したもの
プロダクトコード: テストコードで定義された振る舞いを実装したもの
Rspecでは、TDDの文脈で開発を駆動する「テスト」をソフトウェア設計の要素であると位置付けている。
そしてRspecの狙いは「プログラマにテストコードが設計であることを明確に意識させることと、プログラマがテストコードをスムースに記述し実行できるようになること」。
そのために、Rspec は振舞定義用の DSL を提供し、様々なテスト関連ライブラリや周辺ツールとの連携を積極的に行い、「統合テスティング環境」となることを目指している。
コントローラーのテスト例
まずは、「assings」を使いたいから、下記を実行
1 |
gem 'rails-controller-testing' |
そもそもコントローラーに書く必要があるテストは何?
コントローラーのテストで記述する必要があるのは基本的には下記の3つ。
①受信したリクエストに対して適切なレスポンスを返す
リクエストに対してHTTPレスポンスがステータスコード200を返しているかどうか?
②ビューで使用するのに必要なモデルオブジェクト(インスタンス)をロードする
リクエストされたURLから必要なモデルインスタンスをロードしているかどうか?
適切にインスタンス変数を取り出せているかどうか?
③レスポンスを表示するのに適切なビューを選択する
適切なテンプレートのビューを表示しているかどうか?
indexアクション
※参照URL:https://morizyun.github.io/ruby/rspec-rails-controller.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
describe 'GET #index' do let(:articles) {create_list(:article,2)} before {get: index, params:{}, session:{}} ① it 'has a 200 status code' do expect(assings(:articles)).to have_http_status(:ok) end ② it 'assigns @articles' do expect(assigns(:articles)).to match_array articles end ③ it 'render the :index template' do expect(response).to render_template :index end end |
describe
→describeにはテスト対象を書く。今回の場合は’GET #index’
インスタンス変数の代わりの「let」
上記の例でいうと、
1 |
let(:articles) {create_list(:article,2)} |
のように書くと、 {create_list(:article,2)} の中の値を「articles」として参照して取り出すことができる。
ちなみに {create_list(:articles,2)}はarticlesというインスタンスを2回生成するという意味で、第二引数に繰り返す回数を数字で書く。
https://qiita.com/kindai_dai/items/617fd24b978f1ac1135f
before
example の前処理/ 後処理を記述するためのフック。Railsでいうところの、「before_action」のイメージ
上記の例でいうと、①〜③の検証をする前に {get: index, params:{}, session:{}}を実行している。
{①get: index, ②params:{}, ③session:{}} について
※参照:https://qiita.com/kuboon/items/8a7821e06094706d121c
①get :index 第一引数はアクション名であり、実際の url とはまったく関係がない。
②get :show, id: 3 のようにして、 params[:id] に値を直接渡すことができる。
③post :create, session: {user_id: 5}のようにして、 session: {user_id:} に値を直接渡すことができる。
セッションについて https://www.sejuku.net/blog/33677
assignsメソッド
→assignsメソッドはコントローラーのインスタンス変数をテストするメソッド。引数にインスタンス変数をシンボル型で渡す。
matchマッチャ/match_arrayマッチャ
→文字列・ハッシュ・配列の検証に用いるマッチャ。
matchマッチャで配列を検証すると、配列の順番も期待値(expect)と一致している必要がある。
一方、match_arrayマッチャで検証した場合は、要素の個数と各要素の同値性だけを検証し、順番は検証しない。
コントローラのテストで利用できる機能(一例)
■get(action, params={})
指定したアクションへ GET リクエストを送信。
■response()
レスポンスオブジェクトへのアクセサです。レスポンスオブジェクトは HTTP ステータスコードや使用したテンプレートの情報を保持。
■assigns[variable_name]
コントローラのインスタンス変数 @variable_name に代入されたオブジェクトを取得します。キーには “@” を除いたものを文字列またはシンボルで指定。
■post(action, params={})
指定したアクションへ POST リクエストを送信します。パラメータの指定方法は get と同じです。同様に、put や delete もある。
■session()
セッションオブジェクトへのアクセサです。セッション変数の設定やセッション ID を取得する際に利用。
■request()
リクエストオブジェクトへのアクセサです。リクエストの MIME-Type を設定する場合などに利用。
createアクション
この記事が分かりやすい
URL:https://forest-valley17.hatenablog.com/entry/2018/09/29/174446
※show/edit/newは下記URLを参照。
URL:https://morizyun.github.io/ruby/rspec-rails-controller.html
createアクションで、if @変数.save のように条件分岐をしている場合は、
下記のようにcontextで条件別にグループ化する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
describe 'Post #create' do context "@articleが保存できた時" do it "データベースに値が保存される" do end it "正しいビューに変遷する" do end end context "@articleが保存できなかった時" do it "データベースに値が保存されない" do end it "正しいビューに変遷する" do end end end |
以下、記述例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
describe "#create" do context "as an authenticated user" do before do @user = FactoryBot.create(:user) end it "adds a project" do project_params = FactoryBot.attributes_for(:project) sign_in @user expect do post :create, params: {project: project_params} end.to change(@user.projects, :count).by(1) end end context "as a guest" do it "returns a 302 response" do project_params = FactoryBot.attributes_for(:project) post :create, params: {project: project_params} expect(response).to have_http_status "302" end it "redirects to the sign-in page" do project_params = FactoryBot.attributes_for(:project) post :create, params: {project: project_params} expect(response).to redirect_to "/users/sign_in" end end end |
sign_inを使用して、ログイン状態をシミュレートしてテストをする場合は
spec/rails_helper.rbに下記記述を追加する必要がある。
1 |
config.include Devise::Test::ControllerHelpers, type: :controller |
change + from / to / by
例文
1 2 3 4 5 |
# popメソッドを呼ぶと配列の要素が減少することをテストする x = [1, 2, 3] expect(x.size).to eq 3 x.pop expect(x.size).to eq 2 |
changeで置き換えると、、、
1 2 |
x = [1, 2, 3] expect { x.pop }.to change{ x.size }.from(3).to(2) |
読み方は下記の通り
expect{ X }.to change{ Y }.from(A).to(B) = 「X すると Y が A から B に変わることを期待する」
from().to()は「by」で書き換えることもできる、、、
1 2 |
x = [1, 2, 3] expect { x.pop }.to change { x.size }.by(-1) |
1 2 |
x = [1, 2, 3] expect { x.push(10) }.to change { x.size }.by(1) |
配列要素の個数はsizeじゃなくて、countも使える
1 2 3 4 5 6 7 8 |
class User < ActiveRecord::Base # dependent: :destroy を付けたので、userを削除するとblogも削除される has_many :blogs, dependent: :destroy end class Blog < ActiveRecord::Base belongs_to :user end |
1 2 3 4 5 6 7 |
it 'userを削除すると、userが書いたblogも削除されること' do user = User.create(name: 'Tom', email: 'tom@example.com') # user が blog を書いたことにする user.blogs.create(title: 'Rspec必勝法', content: 'あとで書く') expect {user.destroy}.to change { Blog.count}.by(-1) end |
このように、change マッチャを使うと、「Xに対するある操作が、一見無関係なYに影響を与える」といった検証内容を簡潔に表現することができる。
モック(Mock)
何らかの理由で本物のプログラムが使えない、もしくは使わない方がよいケースでモックが使われる。
使い道としては、外部のAPIを利用しなければならない場合など。
例えば、TwitterのAPI(gem)経由でツイートするプログラムをテストする場合は、テストコードの内容がそのまま
全世界にツイートされてしまう。それは嫌だ。
ということで、モックを使えば、Twitterと同じ機能を持ち且つテストコードでのみ起動するプログラムを作ることができる。
以下、簡単な例
■Ruby.controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 |
# 注:本当に動かす場合はtwitter gemが必要です require 'twitter' class WeatherBot def tweet_forecast twitter_client.update '今日は晴れです' end def twitter_client Twitter::REST::Client.new end end |
■Ruby.Mock.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it 'エラーなく予報をツイートすること' do # Twitter clientのモックを作る(doubleで作れる。) twitter_client_mock = double('Twitter client') # updateメソッドが呼びだせるようにする allow(twitter_client_mock).to receive(:update) # WeatherBotクラスのオブジェクトを作成 weather_bot = WeatherBot.new # twitter_clientメソッドが呼ばれたら上で作ったモックを返すように実装を書き換える allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock) expect{ weather_bot.tweet_forecast }.not_to raise_error end |
そのほか、詳細は下記参照
※参考ページ※ これらを読んだら基本的なRspecのことは分かるはず!
https://morizyun.github.io/ruby/rspec-rails-controller.html
https://qiita.com/jnchito/items/42193d066bd61c740612
https://qiita.com/jnchito/items/640f17e124ab263a54dd
https://qiita.com/jnchito/items/a4a51852c2c678b57868
https://magazine.rubyist.net/articles/0021/0021-Rspec.html
https://qiita.com/namitop/items/bf455f8383181ff6edf3
https://magazine.rubyist.net/articles/0023/0023-Rspec.html#%E6%B0%B4%E5%B9%B3%E5%88%86%E5%89%B2%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81%E3%81%A8%E5%9E%82%E7%9B%B4%E5%88%86%E5%89%B2%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81
https://forest-valley17.hatenablog.com/entry/2018/09/29/174446
https://qiita.com/morrr/items/f1d3ac46b029ccddd017
https://qiita.com/muran001/items/436fd07eba1db18ed622