前回はRailsアプリのモデルテストについて書いたので、
今回はコントローラーのテストについて説明していきます。
コンローラースペックを書き始める
早速、コントローラーのテストを作成していきたいと思います。on the terminal
$ rails g rspec:controller pins
上のコマンドを打つことで、specフォルダ内にcontrollersフォルダが作成され、
その中に、articles_controller_spec.rbが作成されます。
ここでもモデルテストの時と同様に、rspec用に僕のアプリ(github参照)をサンプルとしてテストの紹介をしていきます。
今一度説明しておきますと、このアプリには、記事を登録できるArticleモデルがあり、登録したUserのみがArticleを作成することができるようになっています。
また、未登録のゲストユーザーは、indexのページにしかアクセスできないようになっています。
それでは作成されたファイルを確認してみましょう。
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
end
となっていると思います。
テストを書く前に、まずはこのアプリのコントローラーファイルを見ていきたいと思います。
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index]
before_action :article_owner?, only: [:edit, :update, :destroy]
def index
@articles = Article.all
end
def show
@article = Article.find(params[:id])
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
@article.user_id = current_user.id
respond_to do |format|
if @article.save
format.html { redirect_to article_path(@article), notice: "You successfully created a new article." }
else
format.html { redirect_to new_article_path }
end
end
end
def edit
@article = Article.find(params[:id])
end
def update
@article = Article.find(params[:id])
respond_to do |format|
if @article.update(article_params)
format.html { redirect_to article_path, notice: "You successfully updated your article."}
else
format.html { redirect_to edit_article_path }
end
end
end
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to root_path
end
private
def article_owner?
@article = Article.find(params[:id])
unless @article.user_id == current_user.id
redirect_to root_path
end
end
def article_params
params.require(:article).permit(:title, :text, :user_id)
end
end
コントローラーでテストでは、
ページの表示が正常になされているかどうか
要素の受け渡しが正常になされているかどうか
権限が有効になっていて、且つそれが正常になされているかどうか
といった点を点検していきます。
各アクションごとに点検していくので、その分長くなっていきますし、冗長さに退屈を感じてしまうかもしれませんが、中身は至ってシンプルなので理解しやすいと思います。
権限のないページの、シンプルなテスト
まずは、controller#indexのテストをしていきたいと思います。 indexのページの仕事内容は、上のコードからもわかるように、データベースに格納されたArticlesテーブルのレコードを全て引っ張り出してくる、というものです。 ですので、ここで重要なのは、ページが正常にひらけているかどうか、という点です。RSpec.describe ArticlesController, type: :controller do
describe "#index" do
# 正常なレスポンスか?
it "responds successfully" do
get :index
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
get :index
expect(response).to have_http_status "200"
end
end
end
上記のコードでは、まずはじめに、
indexへのアクセスに対して正常なレスポンスが返ってきているかをテストしています。
indexをgetし、期待したresponseがbe_successであったかどうか、ということです。
次に、
indexのアクセスに対して返ってきたレスポンスが200レスポンスであったかどうかをテストしています。
indexをgetし、期待したresponseがhttp_statusである200をhaveしているかどうか、ということです。
今回テストするアプリのcontroller#indexのテストは以上となります。
非常に簡単だと思いませんか。
しかしながら、ここまでシンプルなのはindexだけです。
indexは、Deviseで生成されたuserの権限とは関係のないページでしたので、比較的分量も少なかったですが、ここに権限の関係が入ってくると、少しずつ記述が長くなっていきます。
権限のあるページの、少し長めのテスト
今度はcontroller#showのテストを実施していきます。describe "#show" do
# 正常なレスポンスか?
it "responds successfully" do
get :show
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
get :show
expect(response).to have_http_status "200"
end
end
showのテストをindexと同じ内容にすると、以下の内容のエラーが発生します。
on the terminal
Failures:
1) ArticlesController#show responds successfully
Failure/Error: get :show
ActionController::UrlGenerationError:
No route matches {:action=>"show", :controller=>"articles"}
# ./spec/controllers/articles_controller_spec.rb:17:in `block (3 levels) in <top (required)>'
2) ArticlesController#show returns a 200 response
Failure/Error: get :show
ActionController::UrlGenerationError:
No route matches {:action=>"show", :controller=>"articles"}
# ./spec/controllers/articles_controller_spec.rb:21:in `block (3 levels) in <top (required)>'
ここではshowの性質について考える必要があります。
そもそも、showのページでは何が表示されることになるのでしょうか?
articles_controller.rbによると、showのアクションでは、
データベースにあるarticlesテーブルから、指定されたidを有するレコードを抽出するように指示しています(以下コード)。
def show
@article = Article.find(params[:id}
end
つまり、このアクションによって呼ばれるページは、articlesテーブルのレコードを抽出するためのidを欲しがっているわけです。
この問題を解決するためには、テストで指定したページであるshowにparams[id]を渡してあげれば良いのです。
describe "#show" do
# 正常なレスポンスか?
it "responds successfully" do
get :show, params: {id: id}
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
get :show, params: {id: id}
expect(response).to have_http_status "200"
end
end
上記のようにidを書けば良いのはわかりましたが、ここで、そもそもarticleのインスタンスが存在しないことに気がつきます。
Article.newをすれば良いわけですが、showのテストの中には、
it "responds successfully"・・・と、it "returns a 200 response"・・・のテストの2種類があります。
1つ1つのためにそれぞれarticleのインスタンスを書くのは、少し面倒なので、
モデルスペックの方で利用したbeforeを活用していきましょう。
describe "#show" do
before do
@article = Article.new(
title: "加藤純一",
text: "加藤純一? 神",
)
end
it "responds successfully" do
get :show, params: {id: @article.id}
expect(response).to be_success
end
it "returns a 200 response" do
get :show, params: {id: @article.id}
expect(response).to have_http_status "200"
end
end
Articleのインスタンスを作成し、そのインスタンスのidをshowアクションに渡しました。
ですが、思い出して欲しいのですが、ArticleにはUserという所有者が必須となっています。
つまり、Articleにはuser_idという外部キーがあるわけです。
この外部キーを作成するために、まずはUserのインスタンスを作成しましょう。
UserはすでにFactoryBotで作成しているので、それを持ってきましょう。
describe "#show" do
before do
@user = FactoryBot.create(:user)
@article = @user.articles.create(
title: "加藤純一",
text: "加藤純一? 神",
)
end
it "responds successfully" do
get :show, params: {id: @article.id}
expect(response).to be_success
end
it "returns a 200 response" do
get :show, params: {id: @article.id}
expect(response).to have_http_status "200"
end
end
# showはすでにデータベースに保存された内容を表示するアクションです。
# before内でarticleのインスタンスを作る際は、create(newとbuildはインスタンスを保存しない)を使います。
持ってきたUserのインスタンスを利用して、Articleをcreateします。
bundle exec rspecでテストを実行しましょう。
すると、・・・・・・エラーが発生します。。。
というのも、controllerのファイルを見返して欲しいのですが、
before_actionで、authenticate_user!が、indexアクションを除いたアクション全てに適用されています。
つまり、Userがログインしていなければ、そもそもそのページに入れないというわけです。
ですが、テスト内でログインするためには、ログイン専用のヘルパーを利用できるようにしないといけません。
RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, type: :controller
end
上記のようにRSpec.configure以下に
config.include Devise::Test::ControllerHelpers, type: :controller
を記入します。
ControllerHelpersというモジュールをincludeすることで、ログインヘルパーを利用可能にしています。
以下のコードを実行すれば、テストが通るはずです。
describe "#show" do
before do
@user = FactoryBot.create(:user)
@article = @user.articles.create(
title: "加藤純一",
text: "加藤純一? 神",
)
end
it "responds successfully" do
sign_in @user
get :show, params: {id: @article.id}
expect(response).to be_success
end
it "returns a 200 response" do
sign_in @user
get :show, params: {id: @article.id}
expect(response).to have_http_status "200"
end
end
on the terminal
ArticlesController
#index
responds successfully
returns a 200 response
#show
responds successfully
returns a 200 response
Article
has a valid factory of user
has a valid factory of another_user
is valid with title, text and user_id
is invalid without title
is invalid without text
is invalid without user_id
does not allow a single user to have articles which has the same title
does not allow a single user to have articles which has the same text
does not allow a single user to have articles which have the same user_id
does allow each user to have an article which has the same title
Finished in 0.37984 seconds (files took 1.66 seconds to load)
14 examples, 0 failures
成功しました!
アクセスしてきたユーザーの場合分け
showのテストでは、Userがログインしているかどうかを確認しましたが、 反対に、ログインしていない人間がshowのページに入ってきたときに、その人がshowページの代わりにログイン画面へリダイレクトされるようになっているのかもテストしておきたいものです。そのためには、登録したUserとゲストとして入ってきたユーザーを場合分けして処理する必要があります。
describe "#show" do
# 権限を有するUserの場合
context "as an authorized user" do
before do
@user = FactoryBot.create(:user)
@article = @user.articles.create(
title: "加藤純一",
text: "加藤純一? 神"
)
end
# 正常なレスポンスか?
it "responds successfully" do
sign_in @user
get :show, params: {id: @article.id}
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
sign_in @user
get :show, params: {id: @article.id}
expect(response).to have_http_status "200"
end
end
# 権限を有しないゲストユーザーの場合
context "as a guest user" do
before do
@user = FactoryBot.create(:user)
@article = @user.articles.create(
title: "加藤純一",
text: "加藤純一? 神"
)
end
# 正常にレスポンスが返ってきていないか?
it "does not respond successfully" do
get :show, params: {id: @article.id}
expect(response).to_not be_success
end
# 302レスポンスが返ってきているか?
it "returns a 302 response" do
get :show, params: {id: @article.id}
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sign_in" do
get :show, params: {id: @article.id}
expect(response).to redirect_to "/users/sign_in"
end
end
end
このように、contextを利用すれば、authorized userとguest userのテストを別々で実行することができるようになるのです。
権限を有するUserの場合のテスト自身の説明はこれ以上しませんが、
ゲストユーザーのテスト側との変更点だけ解説します。
まず、ゲストユーザー側の一番最初のテストでは、
正常にレスポンスが返ってきていないことを確認しています。
中身は権限を有するUser側でのテストとほとんど同じですが、ひとつ違う点は、
expect(response).to_not be_success
となっていて、
responseがbe_successとなっていないことを期待する
という意味になっています。
つまり、ややこしいですが、レスポンスが成功していなければ成功するテストだというわけです。
その次のテストでは、302レスポンスが返っていきているかどうかを確認しています。
ユーザーの認証ができなかったときには、この302レスポンスが返ってくるようになっているので、そこのところは何故302なのか、と悩む必要はありません。これは決まり事です。
権限を有するUser側のテストでは、200になっていたので、これを302にするだけで問題ありません。
最後のテストでは、ゲストユーザーがログイン画面にリダイレクトされているかどうかを確認しています。
showをgetして、paramsにidをセットしたところまではこれまでと変わりありませんが、
期待したresponseがredirect_to "users/sign_in"になっているかをテストしています。
redirect_toマッチャを使っているのがこれまでと異なります。
長くなってしまいましたが、以上のindexとshowの仕組みを理解することができれば、他のアクションについてもテストしやすくなると思います。
前回と同様に、コントローラーテストの例を以下に掲載しておきます。
詳細はgithubでも確認いただけます。
https://github.com/yutaro1204/rspec_demonstration
コントローラーテストの例
改めて説明すると、このアプリには、記事を登録できるArticleモデルがあり、登録したUserのみがArticleを作成することができるようになっています。 また、未登録のゲストユーザーは、indexのページにしかアクセスできないようになっています。RSpec.describe ArticlesController, type: :controller do
before do
@user = FactoryBot.create(:user)
@another_user = FactoryBot.create(:another_user)
@article = FactoryBot.create(:article)
end
describe "#index" do
# 正常なレスポンスか?
it "responds successfully" do
get :index
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
get :index
expect(response).to have_http_status "200"
end
end
describe "#show" do
context "as an authorized user" do
# 正常なレスポンスか?
it "responds successfully" do
sign_in @user
get :show, params: {id: @article.id}
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
sign_in @user
get :show, params: {id: @article.id}
expect(response).to have_http_status "200"
end
end
context "as a guest user" do
# 正常にレスポンスが返ってきていないか?
it "does not respond successfully" do
get :show, params: {id: @article.id}
expect(response).to_not be_success
end
# 302レスポンスが返ってきているか?
it "returns a 200 response" do
get :show, params: {id: @article.id}
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sign_in" do
get :show, params: {id: @article.id}
expect(response).to redirect_to "/users/sign_in"
end
end
end
describe "#new" do
context "as an authorized user" do
# 正常なレスポンスか?
it "responds successfully" do
sign_in @user
get :new
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
sign_in @user
get :new
expect(response).to have_http_status "200"
end
end
context "as a guest user" do
# 正常なレスポンスが返ってきていないか?
it "does not respond successfully" do
get :new
expect(response).to_not be_success
end
# 302レスポンスが返ってきているか?
it "returns a 302 response" do
get :new
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sign_in" do
get :new
expect(response).to redirect_to "/users/sign_in"
end
end
end
describe "#create" do
context "as an authorized user" do
# 正常に記事を作成できるか?
it "adds a new pin" do
sign_in @user
expect {
post :create, params: {
article: {
title: "うんこちゃん",
text: "ねもうすだよなあ?!",
user_id: 1
}
}
}.to change(@user.articles, :count).by(1)
end
# 記事作成後に作成した記事の詳細ページへリダイレクトされているか?
it "redirects the page to /articles/article.id(2)" do
sign_in @user
post :create, params: {
article: {
title: "うんこちゃん",
text: "ねもうすだよなあ?!",
user_id: 1
}
}
expect(response).to redirect_to "/articles/2"
end
end
context "with invalid attributes" do
# 不正なアトリビュートを含む記事は作成できなくなっているか?
it "does not add a new pin" do
sign_in @user
expect {
post :create, params: {
article: {
title: nil,
text: "ねもうすだよなあ?!",
user_id: 1
}
}
}.to_not change(@user.articles, :count)
end
# 不正な記事を作成しようとすると、再度作成ページへリダイレクトされるか?
it "redirects the page to /articles/new" do
sign_in @user
post :create, params: {
article: {
title: nil,
text: "ねもうすだよなあ?!",
user_id: 1
}
}
expect(response).to redirect_to "/articles/new"
end
end
context "as a guest user" do
# 302レスポンスが返ってきているか?
it "returns a 302 request" do
get :create
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sign_in" do
get :create
expect(response).to redirect_to "/users/sign_in"
end
end
end
describe "#edit" do
context "as an authorized user" do
# 正常なレスポンスか?
it "responds successfully" do
sign_in @user
get :edit, params: {id: @article.id}
expect(response).to be_success
end
# 200レスポンスが返ってきているか?
it "returns a 200 response" do
sign_in @user
get :edit, params: {id: @article.id}
expect(response).to have_http_status "200"
end
end
context "as an unauthorized user" do
# 正常なレスポンスが返ってきていないか?
it "does not respond successfully" do
sign_in @another_user
get :edit, params: {id: @article.id}
expect(response).to_not be_success
end
# 他のユーザーが記事を編集しようとすると、ルートページへリダイレクトされているか?
it "redirects the page to root_path" do
sign_in @another_user
get :edit, params: {id: @article.id}
expect(response).to redirect_to root_path
end
end
context "as a guest user" do
# 302レスポンスが返ってきているか?
it "returns a 302 response" do
get :edit, params: {id: @article.id}
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sign_in" do
get :edit, params: {id: @article.id}
expect(response).to redirect_to "/users/sign_in"
end
end
end
describe "#update" do
context "as an authorized user" do
# 正常に記事を更新できるか?
it "updates an article" do
sign_in @user
article_params = {title: "うんこちゃん"}
patch :update, params: {id: @article.id, article: article_params}
expect(@article.reload.title).to eq "うんこちゃん"
end
# 記事を更新した後、更新された記事の詳細ページへリダイレクトするか?
it "redirects the page to /articles/article.id(1)" do
sign_in @user
article_params = {title: "うんこちゃん"}
patch :update, params: {id: @article.id, article: article_params}
expect(response).to redirect_to "/articles/1"
end
end
context "with invalid attributes" do
# 不正なアトリビュートを含む記事は更新できなくなっているか?
it "does not update an article" do
sign_in @user
article_params = {title: nil}
patch :update, params: {id: @article.id, article: article_params}
expect(@article.reload.title).to eq "加藤純一"
end
# 不正な記事を更新しようとすると、再度更新ページへリダイレクトされるか?
it "redirects the page to /articles/article.id(1)/edit" do
sign_in @user
article_params = {title: nil}
patch :update, params: {id: @article.id, article: article_params}
expect(response).to redirect_to "/articles/1/edit"
end
end
context "as an unauthorized user" do
# 正常なレスポンスが返ってきていないか?
it "does not respond successfully" do
sign_in @another_user
get :edit, params: {id: @article.id}
expect(response).to_not be_success
end
# 他のユーザーが記事を編集しようとすると、ルートページへリダイレクトされているか?
it "redirects the page to root_path" do
sign_in @another_user
get :edit, params: {id: @article.id}
expect(response).to redirect_to root_path
end
end
context "as a guest user" do
# 302レスポンスを返すか?
it "returns a 302 response" do
article_params = {
title: "加藤純一",
text: "加藤純一? 神",
user_id: 1
}
patch :update, params: {id: @article.id, article: article_params}
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sign_in" do
article_params = {
title: "加藤純一",
text: "加藤純一? 神",
user_id: 1
}
patch :update, params: {id: @article.id, article: article_params}
expect(response).to redirect_to "/users/sign_in"
end
end
end
describe "#destroy" do
context "as an authorized user" do
# 正常に記事を削除できるか?
it "deletes an article" do
sign_in @user
expect {
delete :destroy, params: {id: @article.id}
}.to change(@user.articles, :count).by(-1)
end
# 記事を削除した後、ルートページへリダイレクトしているか?
it "redirects the page to root_path" do
sign_in @user
delete :destroy, params: {id: @article.id}
expect(response).to redirect_to root_path
end
end
context "as an unauthorized user" do
# 記事を投稿したユーザーだけが、記事を削除できるようになっているか?
it "does not delete an article" do
sign_in @user
another_article = @another_user.articles.create(
title: "じゅん?!",
text: "南原清隆"
)
expect {
delete :destroy, params: {id: another_article.id}
}.to_not change(@another_user.articles, :count)
end
# 他のユーザーが記事を削除しようとすると、ルートページへリダイレクトされるか?
it "redirects the page to root_path" do
sign_in @user
another_article = @another_user.articles.create(
title: "じゅん?!",
text: "南原清隆"
)
delete :destroy, params: {id: another_article.id}
expect(response).to redirect_to root_path
end
end
context "as a guest user" do
# 302レスポンスを返すか?
it "returns a 302 response" do
delete :destroy, params: {id: @article.id}
expect(response).to have_http_status "302"
end
# ログイン画面にリダイレクトされているか?
it "redirects the page to /users/sing_in" do
delete :destroy, params: {id: @article.id}
expect(response).to redirect_to "/users/sign_in"
end
end
end
end
FactoryBot.define do
# 複数のfactoryを書く場合は、以下のように、class: User(model)の部分だけを統一し、
# factoryの後ろの:user, :another_userの部分のように、それぞれのfactoryに名前を付けられます。
factory :user, class: User do
email "test@user"
password "000000"
end
factory :another_user, class: User do
email "test@another_user"
password "000000"
end
end
FactoryBot.define do
factory :article do
title "加藤純一"
text "加藤純一? 神"
user_id 1
end
end
以上のテストでは、わかりやすく書くために、テストとしてはあまり完結ではない書き方をしている箇所も含まれていますので、
より効率的に書ける場所は自分のアプリに合わせて工夫してみてください。
次回は、フィーチャースペック(統合テスト)について解説していきます。