Rails TDD/BDD開発向けテスト一式(ユニット/インテグレーションテスト、テスト自動化、静的解析) Rspec + Capybara + PhantomJS + Poltergeist + Turnip + Jasmine + FactoryGirl + Guard + Rubocop + Spring
概要
Rails4.2 で新規プロジェクトを立ち上げることになったらテストをどうしようか。
現時点で考えうる全部入りを試してみようと思う。
導入する gem やライブラリは下記。
- Rspec
- Capybara
- WEBアプリケーションのテストを補助するライブラリ
- PhantomJS
- ヘッドレスブラウザ
- Poltergeist
- Capybara を PhantomJS で動作させるためのドライバ
- Turnip
- Rspec を Gherkin 書式に対応させる
- Jasmine
- Javascript向けテストフレームワーク
- FactoryGirl
- テストデータを柔軟に生成
- Guard
- ファイル変更を監視して任意のコマンドを実行
- Rubocop
- 静的解析ツール(コーディング規約チェック)
- Spring
- アプリケーションプリローダー
何ができるかというと、
- 単体テスト(Ruby, Javascript)
- 結合テスト
- 静的解析
- 高速自動テスト(ファイル変更がトリガー)
準備
前提
下記を gem install 済み。
gem のインストール
Gemfile を編集。
# vim Gemfile group :development, :test do gem "rspec-rails" gem "factory_girl_rails" gem "guard-rspec" gem "guard-rubocop" gem "spring" gem "guard-spring" gem "spring-commands-rspec" gem "capybara" gem "poltergeist" gem "turnip" gem "jasmine-rails" gem "guard-jasmine" gem "database_cleaner" end
gem install する。
# bundle install
Rspec を利用する準備
# bundle exec rails g rspec:install create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb
テスト用 DB を準備
# bundle exec rake db:test:prepare
Guardfile の生成
# bundle exec guard init rspec # bundle exec guard init rubocop # bundle exec guard init jasmine
Guardfile が作成されたことを確認しつつ(guard :rspec
guard :jasmine
guard :rubocop
の項目があること)、
spec, feature ファイルも監視対象にする。
# vim Guardfile # ↓springに対応するように spring rspec へ書き換え guard :rspec, cmd: "bundle exec spring rspec" do ... watch(%r{^spec/.+_spec\.rb}) watch(%r{^spec/features/(.+\.feature)}){ |m| "spec/features/#{m[1]}" } ...
Rubocop の設定
Rubocop 設定用ファイルを生成。
# bundle exec rubocop --auto-gen-config ... Created .rubocop_todo.yml. Run `rubocop --config .rubocop_todo.yml`, or add inherit_from: .rubocop_todo.yml in a .rubocop.yml file.
.rubocop_todo.yml
が生成されるので、
リネームして Rails 用オプションを追加。
# mv .rubocop_todo.yml .rubocop.yml # vim .rubocop.yml AllCops: RunRailsCops: true Exclude: - 'vendor/**/*' - 'spec/steps/*' ...
後々必要になった Exclude の設定も書いておく。
step ファイルを対象外に設定したいのだが、デフォルト値が上書きされる問題がある。
詳細は下記。
Ruby - rubocop gemを使うためにたった1つの重要なこと - Qiita
Turnip 対応
.rspec に設定を追加して、.feature
のファイルもテスト対象にする
# vim .rspec -r turnip/rspec
turnip_helper.rb を追加して、step
ファイルもテスト対象にする。
# vim spec/turnip_helper.rb require 'rails_helper' # steps ファイルの設置場所を指定する Dir.glob("spec/steps/*step.rb") { |f| load f, true }
jasmine の設定
jasmine の設定ファイルを追加して、テストを動作してみる。
# bundle exec rails g jasmine_rails:install identical spec/javascripts/support/jasmine.yml route mount JasmineRails::Engine => '/specs' if defined?(JasmineRails) # RAILS_ENV=test bundle exec rake spec:javascript Starting... Finished ----------------- 0 specs, 0 failures in 0.001s.
spring の設定
spring を rspec に対応させる。
# bundle exec spring binstub --all * bin/rake: spring already present * bin/rspec: generated with spring * bin/rails: spring already present
下記コマンドが叩けることを確認する。
# ↓spring を起動して rspec を流す # bundle exec spring rspec # ↓spring 環境が立ち上がっていることを確認 # bundle exec spring status
エラーが出る場合は先に下の項目をやる。
spec_helper の設定
Capybara と Poltergeist と DatabaseCleaner の設定を追加する。
DatabaseCleaner はインテグレーションテストする際には必須で、
データベースにデータが残るのを初期化してくれる。
Rspec でのテストは設定にもよるが、基本的にはトランザクションをコミットしないで処理するため、
データが残らない仕組みになっている。
下記は行頭に追加。
# vim spec/spec_helper.rb require 'capybara' require 'capybara/rspec' require 'capybara/poltergeist' require 'database_cleaner' Capybara.register_driver :poltergeist do |app| Capybara::Poltergeist::Driver.new(app, :js_errors => true, :timeout => 60) end Capybara.configure do |config| config.default_driver = :poltergeist config.javascript_driver = :poltergeist end ... RSpec.configure do |config| ... config.before(:suite) do DatabaseCleaner.strategy = :truncation DatabaseCleaner.clean_with(:truncation) end ...
テスト用の一式を作成
# bundle exec rails g scaffold test_dayo name:string invoke active_record create db/migrate/20150611054639_create_test_dayos.rb create app/models/test_dayo.rb invoke rspec create spec/models/test_dayo_spec.rb invoke factory_girl create spec/factories/test_dayos.rb invoke resource_route route resources :test_dayos invoke scaffold_controller create app/controllers/test_dayos_controller.rb invoke erb create app/views/test_dayos create app/views/test_dayos/index.html.erb create app/views/test_dayos/edit.html.erb create app/views/test_dayos/show.html.erb create app/views/test_dayos/new.html.erb create app/views/test_dayos/_form.html.erb invoke rspec create spec/controllers/test_dayos_controller_spec.rb create spec/views/test_dayos/edit.html.erb_spec.rb create spec/views/test_dayos/index.html.erb_spec.rb create spec/views/test_dayos/new.html.erb_spec.rb create spec/views/test_dayos/show.html.erb_spec.rb create spec/routing/test_dayos_routing_spec.rb invoke rspec create spec/requests/test_dayos_spec.rb invoke helper create app/helpers/test_dayos_helper.rb invoke rspec create spec/helpers/test_dayos_helper_spec.rb invoke jbuilder create app/views/test_dayos/index.json.jbuilder create app/views/test_dayos/show.json.jbuilder invoke assets invoke coffee create app/assets/javascripts/test_dayos.js.coffee invoke scss create app/assets/stylesheets/test_dayos.css.scss invoke scss identical app/assets/stylesheets/scaffolds.css.scss
テスト実行
テストを実行してみるが、エラーが発生。
# bundle exec rspec DEPRECATION WARNING: The configuration option `config.serve_static_assets` has been renamed to `config.serve_static_files` to clarify its role (it merely enables serving everything in the `public` folder and is unrelated to the asset pipeline). The `serve_static_assets` alias will be removed in Rails 5.0. Please migrate your configuration files accordingly. (called from block in <top (required)> at /hoge_app/config/environments/test.rb:16) Migrations are pending. To resolve this issue, run: bin/rake db:migrate RAILS_ENV=test
エラーの対応
エラーの通り設定を修正
# vim config/environments/test.rb # config.serve_static_assets = true config.serve_static_files = true
エラーの通り TestDayo モデルを追加したのでマイグレートする
# bundle exec rake db:migrate # RAILS_ENV=test bundle exec rake db:migrate
再びテスト実行
# bundle exec rspec ... Finished in 3.33 seconds (files took 2.27 seconds to load) 30 examples, 0 failures, 17 pending
Rspec によるテスト
Guard起動
Guard を実行しながらテストを書いてみる。
# bundle exec guard
最初に rubocop が動作するが、一旦無視w
iTerm 使っているならcmd + shift + d
とかで画面分割するといい。
# vim spec/models/test_dayo_spec.rb
適当に編集し、:w
で保存の度に Guard で Rspec が実行されることを確認。
rubocop が若干うるさいので、Rspec が通ったら Rubocop が実行されるようにする。
# vim Guardfile group :red_green_refactor, halt_on_fail: true do ... guard :rspec, cmd: "bundle exec spring rspec" do ... guard :rubocop do ... end guard :jasmine do ...
yujinakayama/guard-rubocop · GitHub
↑詳細はここに。
モデルのテストを書く
1つ目は FactoryGirl
によって生成されたデータのテスト、通るテスト。
2つ目は namaedesu
という存在しないメソッドのテスト。
3つ目は valid?
メソッドを正しい/正しくないパラメータを与えてテスト。
# vim spec/models/test_dayo_spec.rb RSpec.describe TestDayo, type: :model do subject(:subject) { FactoryGirl.create(:test_dayo) } describe "#name" do it { expect(subject.name).to eq "MyString" } end describe "#namaedesu" do it { expect(subject.namedesu).to eq "MyStringdesu"} end describe "#valid?" do context "given valid_param" do let(:valid_param) { {name: "name"} } subject(:subject) { FactoryGirl.build(:test_dayo, valid_param).valid? } it { expect(subject).to be true } end context "given invalid_param" do let(:invalid_param) { {name: ""} } subject(:subject) { FactoryGirl.build(:test_dayo, invalid_param).valid? } it { expect(subject).to be false } end end end
guard の画面はこんな感じ。
モデルにメソッドを実装していないし、validate
の定義がないため落ちる。
- INFO - Running: spec/models/test_dayo_spec.rb .F.F Failures: 1) TestDayo#namaedesu Failure/Error: it { expect(subject.namaedesu).to eq "MyStringdesu"} NoMethodError: undefined method `namaedesu' for #<TestDayo:0x007f9575946530> # ./spec/models/test_dayo_spec.rb:10:in `block (3 levels) in <top (required)>' 2) TestDayo#valid? given invalid_param should equal false Failure/Error: it { expect(subject).to be false } expected false got true # ./spec/models/test_dayo_spec.rb:23:in `block (4 levels) in <top (required)>' Finished in 0.02159 seconds (files took 3.12 seconds to load) 4 examples, 2 failures
モデルを実装
# vim app/models/test_dayo.rb class TestDayo < ActiveRecord::Base validates :name, presence: true def namaedesu "#{name}desu" end end
Guard でテストが通っていることを確認。
4 examples, 0 failures
コントローラーのテスト
とりあえず、テストを流し見てると
16 examples, 0 failures, 15 pending
15 のテストが pending 状態になっている。
それらをテストされるように修正していく。
skip
で検索すると設定すべき項目がわかるので、それらを修正していく。
# vim spec/controllers/test_dayos_controller_spec.rb ... let(:valid_attributes) { #skip("Add a hash of attributes valid for your model") { name: "namae" } } let(:invalid_attributes) { #skip("Add a hash of attributes invalid for your model") { name: "" } } ... describe "PUT #update" do context "with valid params" do let(:new_attributes) { #skip("Add a hash of attributes valid for your model") { name: "aiueo"} } it "updates the requested test_dayo" do ... #skip("Add assertions for updated state") expect(assigns(:test_dayo)).to eq(test_dayo) end ...
テストが全て通るようになることを確認。
16 examples, 0 failures
ヘルパーのテスト
ヘルパー単体でテストを流すと pending が1つと出るので、 適当なテストケースを作る。
# vim spec/helpers/test_dayos_helper_spec.rb ... #pending "add some examples to (or delete) #{__FILE__}" describe "#kuso" do it { expect(helper.kuso("a-")).to eq "a-kuso" } end
1 example, 1 failure
ヘルパーメソッドを実装していないのでエラーが出るので実装。
# vim app/helpers/test_dayos_helper.rb module TestDayosHelper def kuso(text) "#{text}kuso" end end
テストが通ることを確認。
1 example, 0 failures
Capybara, Turnip によるテスト
デフォルトで用意されている welcom/index のページで簡単なテストを書く。
フィーチャーファイルの作成、utf-8で作成すること。
# vim spec/features/welcome.feature
# language: ja 機能: ウェルカムページ シナリオ: ウェルカムページへアクセスする 前提ウェルカムページへアクセスする ならば 画面にWelcomeと表示されていること
ステップファイルの作成。
# vim spec/steps/welcome_step.rb
# encoding: utf-8 step 'ウェルカムページへアクセスする' do visit '/welcome/index' end step '画面にWelcomeと表示されていること' do expect(page).to have_content('Welcome') end
テストが通ることを確認。
1 example, 0 failures
ステップファイルは使い回し可能な書き方ができるようなので、 プロジェクトで使う際には下記参考。
hachi8833/turnip_generic_steps · GitHub
↑を利用して、テストを書く。
# vim spec/steps/generic_step.rb # 上記の汎用ステップファイルを持って来て下記を追加。 # 操作用ステップ step %(:pageページにアクセスする) do |page| visit "#{page}" end
feature ファイルの追加。
行頭の# language: ja
とファイルのエンコードをutf-8
にすること。
# vim spec/features/test_dayo.feature # language: ja 機能: テストだよ シナリオ: 一覧ページを開く 前提"/test_dayos"ページにアクセスする ならば"Listing Test Dayos"と表示されている シナリオ: 新規作成ページで新規登録を行う 前提"/test_dayos"ページにアクセスする ならば"New Test dayo"リンクをクリックする かつ"New Test Dayo"と表示されている かつ"test_dayo[name]"に"名前"を設定する かつ"Create Test dayo"ボタンをクリックする ならば"Test dayo was successfully created."と表示されている シナリオ: 新規作成で追加したデータが存在し、詳細ページを開ける 前提"/test_dayos"ページにアクセスする ならば"名前"と表示されている かつ"Show"リンクをクリックする ならば"Name: 名前"と表示されている シナリオ: 新規作成したデータを編集して保存する 前提"/test_dayos"ページにアクセスする ならば"Edit"リンクをクリックする ならば"Editing Test Dayo"と表示されている かつ"test_dayo[name]"に"名前です"を設定する かつ"Update Test dayo"ボタンをクリックする ならば"Test dayo was successfully updated."と表示されている かつ"Back"リンクをクリックする ならば"名前です"と表示されている シナリオ: 作成したデータを削除する 前提"/test_dayos"ページにアクセスする ならば"Destroy"リンクをクリックする かつ"Test dayo was successfully destroyed."と表示されている
5つのシナリオテストが通っていることを確認。
5 examples, 0 failures
ちなみに Destroy ボタンを押したときに Javascript の Confirm ポップアップが出るが、 poltergeist を利用していると自動で OK が選択される。
featureスペックで確認ダイアログをクリックしたい - ツユダクの肉増しのRuby on Railsの初心者で
Jasmine でのテスト
js の spec ファイルを作成
# vim spec/javascripts/test_dayos_spec.js.coffee
//= require test_dayos describe "TestDayo", -> beforeEach -> @test_dayo = new TestDayo("namae") it "name test", -> expect(@test_dayo.name).toBe "namae"
Guard ではエラーが発生する。
- INFO - TestDayo - INFO - ✘ name test - INFO - ➤ ReferenceError: Can't find variable: TestDayo - INFO - ➜ test_dayos_spec.js.coffee on line 8 - INFO - ➤ TypeError: 'undefined' is not an object (evaluating 'this.test_dayo.name') - INFO - ➜ test_dayos_spec.js.coffee on line 11 - ERROR - 1 spec, 1 failure
coffeescript で TestDayo
クラスを実装をする。
# vim app/assets/javascripts/test_dayos.js.coffee
class @TestDayo constructor: (name) -> @name = name name: -> @name
Jasmine のテストが通ることを確認。
- INFO - 1 spec, 0 failures
Guard を起動すると、Jasmine 用のサーバも立つので、
http://localhost:8888 とかでブラウザでテストを確認することもできる。
まとめ
一通りテストが動作するところまでの環境作りができた。
これだけのテスト環境を用意すると、快適な TDD/BDD を進めていけると思う。