読者です 読者をやめる 読者になる 読者になる

Shred IT!!!!

IT全般について試したこと・勉強したことを綴ったり、趣味について語るブログ

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
  • FactoryGirl
    • テストデータを柔軟に生成
  • Guard
    • ファイル変更を監視して任意のコマンドを実行
  • Rubocop
    • 静的解析ツール(コーディング規約チェック)
  • Spring
    • アプリケーションプリローダー

何ができるかというと、

準備


前提
下記を 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

coffeescriptTestDayo クラスを実装をする。

# 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 を進めていけると思う。