capybaraで画像アップロード機能の統合テストをしたい

こんにちは!kossyです!





本日のニュースではないですが、気になる技術を見つけたので。
aws.amazon.com

昨日のWBSで、このリンク先にある導入事例の企業が紹介されており、
AWSって画像認識までクラウドで提供してるのかよ!」
と驚いてしまいました。

事例では、従来は幼稚園の児童を撮影した写真を地元の写真屋さんが撮影・現像し、
園内で販売するという手法を取っていたそうですが、
AWSのサービスを利用し、集団の写真の中で我が子の映った写真のみを素早く抽出、
一覧で表示することにより、写真を買う保護者が増えているそう。

行事だけでなく、普段の様子もプロのカメラマンに撮影を依頼し、
普段は見ることのできない我が子の園内での様子を納めた写真は、
保護者から好評いただいているという。

AWSのように複雑な機能はクラウドで提供されているので、
いかにしてツールを組み合わせて、ユーザーに新しい体験を提供するかが
大事になっているなと感じます。










さて、今回は、capybaraで画像アップロード機能を含めた、
投稿機能の統合テストの実装方法をブログに残してみたいと思います。



環境
Rails 5.1.6
Ruby 2.5.1
Rspec 3.8.0
FactoryBot 4.11.1
Devise 4.5.0
capybara 2.18.0
carrierwave 1.2.3
rmagick 2.16.0
MacOS Mojave



尚、各種gemの導入は済んでいるものとし、capybaraが何のために使われるツールなのかは
理解しているものとします。






まずはFactoryBotを使ってデータ生成

まずはspecディレクトリ下にfactoriesというディレクトリを作成し、
user.rbという名前のファイルを作成します。

中身はこんな感じです。

spec/factories/user.rb



FactoryBot.define do
  factory :user do
    name { "test_user" }
    email { "12345678@gmail.com" }
    password { 12345678 }
  end
end



post_spec.rbの完成版コード
specディレクトリ下に、featuresというディレクトリを作成し、
その中にpost_spec.rbというファイルを作成します。

先に完成版のコードを置いておきます。

spec/features/post_spec.rb



require 'rails_helper'

feature 'user', type: :feature do
  let(:user) { create(:user) }

  scenario 'post picture' do
    visit root_path
    expect(page).to have_no_content('投稿')

    visit new_user_session_path
    fill_in 'user_email', with: user.email
    fill_in 'user_password', with: user.password
    find('input[name="commit"]').click
    expect(current_path).to eq root_path
    expect(page).to have_content('投稿')

    expect {
      click_link('投稿')
      expect(current_path).to eq new_post_path
      find('input[type="file"]').click
      attach_file "images[]", "app/assets/images/icon.png"
      fill_in 'post[caption]', with: 'フィーチャスペックのテスト'
      find('input[type="submit"]').click
    }.to change(Post, :count).by(1)
  end
end

各コードについて補足説明をしていきます。

1行目のrequire 'rails_helper'ですが、
これはもうrspecを使う時に必ず書くおまじないだと思ってください。


feature 'user', type: :feature do
  let(:user) { create(:user) }

featureはdescribeと同じようなものだと思ってください。
どちらもエイリアスメソッドですが、統合テストを行う時はfeatureを使うのが一般的なようです。
let(:user) { create(:user) }で先ほど定義したuser.rbの内容に準じてuserモデルのインスタンスを作成します。
以降、user.カラム名とかで値を取り出せるようになります。


    visit root_path
    expect(page).to have_no_content('投稿')

visitメソッドは引数にURL、 もしくはプレフィックスを指定することで、
該当ページに移動することができるメソッドです。めっちゃ便利です。

今回はアプリの仕様上、ログインしないと新規投稿ページへのリンクが表示されない仕様になっているので、
expectメソッドの引数にpageを指定し、投稿という文字列がルートパスのページに
表示されていないかテストをしています。


    visit new_user_session_path
    fill_in 'user_email', with: user.email
    fill_in 'user_password', with: user.password
    find('input[name="commit"]').click
    expect(current_path).to eq root_path
    expect(page).to have_content('投稿')

visitメソッドを使ってログインページに遷移し、
fill_inメソッドを使ってinput要素の中身に値を入れています。
第一引数にはname属性の値を指定し、第二引数にwithでinput要素に入れたい値を代入しています。

findメソッドは、引数に指定したものをHTMLの中から探し出すメソッドです。
今回はclickメソッドを使って引数に指定した'input[name="commit"]'をクリックする動作を再現しています。

ログイン成功後、現在のページがルートパスであることを確認し、
そのページのHTMLの中に「投稿」という文字列があることを確認しています。


    expect {
      click_link('投稿')
      expect(current_path).to eq new_post_path
      find('input[type="file"]').click
      attach_file "images[]", "app/assets/images/icon.png"
      fill_in 'post[caption]', with: 'フィーチャスペックのテスト'
      find('input[type="submit"]').click
    }.to change(Post, :count).by(1)
  end
end

expectは{}内でまとめてテストを行うこともできます。

まず、投稿というリンクをクリックし、
新規投稿ページにアクセスしていることを確認しています。

findメソッドを使って、当該ページのHTMLの中から
input[type="fileというinput要素を探し、クリックします。

その後、attach_fileメソッドを使って、第一引数に指定した 'images[]' の中身に
"app/assets/images/icon.png"を代入しています。
画像アップロードのテストを行う時はfill_inじゃダメみたいです、、、(これで30分はハマりました)

fill_inメソッドでcaptionの中身を埋めて、
findで'input[type="submit"]'を探し出してクリックし、
to changeメソッドの引数にPostモデルのレコードの数を指定し、byメソッドで中身が1つ変わったことを
テストして終了です。





まとめ
テスト通ると気持ちいいですよね。
ブラウザで思った通りの動きをするのかチェックする時とは
また違った快感があります。笑

間違いやご質問等ございましたらコメント頂ければ幸いでございます。





参考にさせていただいた記事
RSpec で capybara を使用してファイルアップロードをする場合 - Qiita
Capybaraでファイルアップロードのテストを書く - Qiita