スクラムの受け入れ条件と対応させてTurnipで自動テストを書く

2015.07.07

丹内です。

掲題の通り、受け入れ条件を意味のあるものにするため、開発者が書く自動テストを意味のあるものにする話をします。

受け入れ条件とは

受け入れ条件とは、スプリントの計画時に決める「これを満たせば達成とする」という条件のことです。

チームやスクラムの運用によって内容は変わると思うのですが、私は「期待される成果物や機能を具体的に書くこと」が、受け入れ条件にとって必要なことだと考えています。

この受け入れ条件は、例えば「ユーザはタグを付けてブログを投稿できる」というストーリーの場合、以下のようになります。

  • タイトルと本文から成るブログを投稿できること
  • タグを付けて投稿することができること
  • ユーザマニュアルが更新されていること
  • このうち上の2つは具体的な機能に関する受け入れ条件ですが、これと対応するように自動テストを書きたいと思います。

    Turnipとは/受け入れ条件との関連

    Turnipとは、RSpec向けのGherkin拡張(Gherkin extension for RSpec)です。

    Cucumberの親戚なのですが、Cucumberより簡単に記述することができると言われています(私はCucumberを使ったことが無いです。スミマセン)。

    Turnipの詳細は以下のリンクが詳しいので、そちらを御覧ください。

    エンドツーエンドテストの自動化は Cucumber から Turnip へ - RubyMagazine

    Turnipでは具体的な操作(フォームに入力する、ボタンをクリックするなど)に日本語や英語の名前をつけて、それを別のファイルでテストとして記述することで、自動テストを実現しています。

    直接specファイルに書いても同じことができますが、Turnipで書くテストをスクラムの受け入れ条件と一致させることで、

  • 開発者から見れば、受け入れ条件を満たす自動テストを書くことを意識しやすくなる
  • 非開発者から見れば、自動テストが何をしているのか理解しやすくなる
  • というメリットがあります。

    例えば、以下の様なRSpecがあったとします。

    describe 'ブログ投稿' do
      let(:title) { 'ブログタイトル' }
      let(:entry) { 'ブログ本文' }
      
      before { visit root_path }
      
      context 'ブログを投稿したとき' do
        subject { page }
        
        before do
          click_link I18n.t('navbar.new')
          fill_in 'blog[title]', with: title
          fill_in 'blog[entry]', with: entry
          click_button I18n.t('helpers.submit.create')
        end
      
        it 'ブログが表示されること' do
          expect(subject).to have_content title
          expect(subject).to have_content entry
        end
      end
    end

    これをTurnipで書く場合、まずstepを定義します。

    steps_for :blog do
      step 'rootに移動する' do
        visit root_path
      end
      
      step 'ナビゲーションバーから投稿画面に移動する' do
        click_link I18n.t('navbar.new')
      end
      
      step 'タイトルとエントリを入力して投稿ボタンを押す' do
        fill_in 'blog[title]', with: 'test title'
        fill_in 'blog[entry]', with: 'test entry'
        click_button I18n.t('helpers.submit.create')
      end
      
      step 'ブログが表示されること' do
        expect(page).to have_content 'test title'
        expect(page).to have_content 'test entry'
      end
    end

    次に、featureファイルにテストを書きます。

    Feature: ブログの投稿
      @blog
      Scenario: ユーザはブログを投稿できる
      When rootに移動する
      And ナビゲーションバーから投稿画面に移動する
      And タイトルとエントリを入力して投稿ボタンを押す
      Then ブログが表示されること

    このfeatureファイルの内容を、ストーリーの受け入れ条件と一致させてやることで、仕様と実装を一致させやすくなります。

    ことスクラムにおいては、

  • 価値を定義しながら仕様を考えつつ実装していく
  • 開発者以外にも、プロダクトオーナーやスクラムマスターなど様々な非開発者ロールの方がいる
  • という点で、非開発者にもリーダブルなテストを書くことが重要だと思います。

    また、逆に、非開発者でもテストやテストレポートを読むことで、開発者との距離を縮めることができます。

    Turnipのセットアップ

    今回はRailsの場合を想定します。

    $ rbenv version
    2.2.2 (set by /Users/tannaiyuki/.rbenv/version)
    
    $ rbenv exec rails -v
    Rails 4.2.2
    
    $ rbenv exec rails new TurnipSample --test=rspec --database=mysql --skip-bundle
    ...

    ここから、Turnipというgemを使えるようにセットアップします。

    まずはGemfileを修正しましょう。以下を追記して、bundle installをします。

    group :test do
      gem 'rspec-rails'
      gem 'capybara'
      gem 'database_rewinder'
      gem 'turnip'
    end

    次に、RSpec/Capybara/Turnipを使うための設定を行います。

    各種設定

    RSpecをインストールします。

    $ bundle exec rails g rspec:install

    rspecの設定をします。

    # This file is copied to spec/ when you run 'rails generate rspec:install'
    ENV['RAILS_ENV'] ||= 'test'
    require 'spec_helper'
    require File.expand_path('../../config/environment', __FILE__)
    require 'rspec/rails'
    
    # 以下の設定を追加する
    require 'capybara/rails'
    require 'turnip'
    require 'turnip/rspec'
    require 'turnip/capybara'
    Dir.glob('spec/steps/**/*steps.rb') { |f| load f, true }
    
    ...
    
    # 以下も追加
    RSpec.configure do |config|
      # database_rewinder gem
      config.before :suite do
        DatabaseRewinder.clean_all
      end
    
      config.after :each do
        DatabaseRewinder.clean
      end
    end

    Turnipはspec/turnip_helper.rbの設定を読んで動作するので、このファイルを追加します。ここではrails_helperの設定を引き継ぐようにします。

    require 'rails_helper'

    そして、rspecコマンドでturnipも実行されるように、.rspecに追記をします(無ければ作成です)。

    --require spec_helper
    -r turnip/rspec

    動作確認してみます。

    $ bundle exec rake db:create
    $ bundle exec rspec
    No examples found.
    
    
    Finished in 0.00038 seconds (files took 0.57307 seconds to load)
    0 examples, 0 failures

    正常終了したら準備完了です。

    Scaffoldしてテストを書いてみる

    $ bundle exec rails g scaffold blog title:string entry:text
          invoke  active_record
          create    db/migrate/20150707084213_create_blogs.rb
          create    app/models/blog.rb
          invoke    test_unit
          create      test/models/blog_test.rb
          create      test/fixtures/blogs.yml
    (以下、省略)
    
    $ bundle exec rake db:migrate
    == 20150707084213 CreateBlogs: migrating ======================================
    -- create_table(:blogs)
       -> 0.0291s
    == 20150707084213 CreateBlogs: migrated (0.0291s) =============================

    rootも設定します。

    Rails.application.routes.draw do
      resources :blogs
      root 'blogs#index'   <- 手で追記する

    rails serverでscaffoldの画面が見れればOKです。

    いよいよテストを書きます。

    今回は、受け入れ条件を以下のように決めます。これは本来、スクラムイベントによって決定されるものです。

  • ストーリー:「ユーザはブログ記事を作成できる。(なぜなら◯◯だから。)」
  • 受け入れ条件:「タイトルと本文を入力して記事を作成できること」
  • 最初に、受け入れ条件をfeatureに書き写します。 spec/features/blog.feature です。

    Feature: ユーザはブログ記事を作成できる
      @blog
      Scenario: タイトルと本文を入力して記事を作成できること
      When トップを訪問する
      And Newリンクをクリックする
      And タイトルと本文を入力して作成ボタンを押す
      Then 作成したタイトルが表示されていること
      And 作成した本文が表示されていること

    この時点でテストを実行しても、stepを実装していないのでpendingされます。以下のように出力されます。

    $ bundle exec rspec
    *
    
    Pending: (Failures listed here are expected and do not affect your suite's status)
    
      1) ユーザはブログ記事を作成できる タイトルと本文を入力して記事を作成できること When トップを訪問する -> And Newリンクをクリックする -> And タイトルと本文を入力して作成ボタンを押す -> Then 作成したタイトルが表示されていること -> And 作成した本文が表示されていること
         # No such step: 'トップを訪問する'
         # ./spec/features/blog.feature:4
    
    
    Finished in 0.01698 seconds (files took 5.62 seconds to load)
    1 example, 0 failures, 1 pending

    では、stepを実装しましょう。ファイル名は spec/steps/blog_steps.rb です。

    steps_for :blog do
      step 'トップを訪問する' do
        visit root_path
      end
    
      step 'Newリンクをクリックする' do
        click_link 'New Blog'
      end
    
      step 'タイトルと本文を入力して作成ボタンを押す' do
        fill_in 'blog[title]', with: 'test title'
        fill_in 'blog[entry]', with: 'test entry'
        click_button 'Create Blog'
      end
    
      step '作成したタイトルが表示されていること' do
        expect(page).to have_content 'test title'
      end
    
      step '作成した本文が表示されていること' do
        expect(page).to have_content 'test entry'
      end
    end

    テストを実行すると、以下のようにPASSすることがわかります。

    $ bundle exec rspec -fd
    
    ユーザはブログ記事を作成できる
      タイトルと本文を入力して記事を作成できること
        When トップを訪問する -> And Newリンクをクリックする -> And タイトルと本文を入力して作成ボタンを押す -> Then 作成したタイトルが表示されていること -> And 作成した本文が表示されていること
    
    Finished in 0.22194 seconds (files took 2.05 seconds to load)
    1 example, 0 failures

    Turnipの使いドコロ

    TurnipやRSpec/Capybaraを使ったこのような自動テストは、以下の様なデメリットがあります。

  • テストを構造化しづらい(DRYに記述しづらい)
  • ユニットテストに比べて実行に時間がかかる
  • ひたすらにこのようなテストを書いていくと、いずれテストの実行時間やメンテにかかるコストが大きくなってしまいます。

    何を以って「意味のあるテスト」であるかは人によって定義が変わる中でその指標としてスクラムノ受け入れ条件を活用すると良いと思います。

    受け入れ条件以外の項目はユニットテストとしてガンガン書いて、開発を加速させましょう。

    まとめ

    スクラムの受け入れ条件と整合性のとれる自動テストを記述する観点を記述しました。

    価値を定義しながら実装すると、書き換えやコンフリクトのためテストの必要性が大きくなります。

    素早く意味のあるテストを書いて開発を加速させ、実現できる価値を最大化しましょう。