GherkinとはCucumberやTurnipで採用されているテスト記述言語フォーマットのことです。RSpec vs MiniTest等の議論は巷では活発に行われていますが、Gherkinに関しては余り大きく取りあげられたりすることがないように感じます。
しかし、Gherkinは以下の様な理由から是非採用した方が良いテスト手法だと考えています:
- 自然言語で書ける。英語でも日本語でもOK
- テスト仕様部分とロジック部分をfeatureとstepで分離しており何がテストされているか分かり易い
それではRSpec、MiniTest、Gherkin(Turnip)でそれぞれ同じテストケースを記述してみるとどういう書き方になるのかを検証していきます。
目次
テストサンプルによるMiniTest / RSpec / Gherkin比較
以下はrails g scaffold post title:string body:string
で生成したPost CRUDを対応したときの投稿一覧画面のIntegrationテストケースです:
MiniTest
class PostsControllerTest < ActionDispatch::IntegrationTest
test "投稿一覧ページが正しく表示出来る" do
get posts_url
assert_response :success
end
end
MiniTestは3つの比較の中では最もシンプルでRubyでベタっと記述することが出来ます。テスト仕様に関してもtest "投稿一覧ページが正しく表示出来る"
の部分を見れば一見分かるように思えますが、具体的にどう操作すればどう挙動するかという部分については上から順にコードを読まなくてはなりません。しかし、読めば分かる、かつ余計なDSLを覚える必要が少ないという点ではgemをバリバリ書くようなRubyマスターな人達には好まれるのかもしれません...
RSpec
RSpec.describe "投稿", type: :request do
describe "一覧画面を開く" do
before { get posts_path }
it("正しく表示出来る") { expect(response).to have_http_status(200) }
end
end
RSpecではdescribe
でテスト対象、context
で条件やbefore
による前提条件、subject
でテスト対象、it
でテスト内容を記述してマッチャexpect(response).to have_http_status(200)
のように自然言語に近い状態でどういう振る舞いかを記述することができ、流石BDDの立役者にふさわしいテスト記述法になっています。また、上記のサンプルを--format documentation
で実行するとテスト実行結果が:
投稿
一覧画面を開く
正しく表示出来る
と表示され、テスト内容も見やすいように思えます。しかし、describe
やcontext
やit
はネストさせて記述する事が出来、記述が複雑になってきた場合に、では実際に1テストケースがどのようなテストなのかを把握しながらRSpec記述を行っていくことは困難になっていくこと、RSpec環境を整備して実行しないとテスト内容を確認出来ない点などが不利になっていきそうです。
Gherkin
posts.feature
機能: 投稿
シナリオ: 一覧画面が正しく表示出来る
もし 投稿一覧ページを表示する
ならば 正しく表示出来る
step.rb
step '投稿一覧ページを表示する' do
visit '/posts'
end
step '正しく表示出来る' do
expect(page).to have_http_status(200)
end
今回の本題であるGherkinでの記述です。Cucumberでも良いのですが今回の例はTurnipです。ポイントは仕様とロジックが分離出来ている点で、どのようなテストをしているかを知りたい場合はfeatureファイルを参照するのみ、Rubyの知識は要らないといったメリットがあります。
デメリットはないのでしょうか?分離してしまう副作用でMiniTestに比べれば全体のコードは複雑になっていきます。開発者が自分だけしかいない等の小規模プロダクトだとか、そもそもプロダクトじゃないとかだと採用するメリットも薄れるかもしれないです。
Gherkinで書いておけばなんのテストをしているかすぐに分かる
というわけでGherkin推しなわけです。以下はOGPをどのように記述しておくかプロダクトで使っている1シナリオをサンプルとして挙げておきます:
シナリオ: 投稿・サムネイル情報がメタで提供されている
SNSにシェアされた投稿を見て、クリックしたくなるように視覚的に誘導したい
og:titleを投稿時のコメントとし、サイトの内容が把握しやすいようにする
og:description、descriptionメタは空白とし、シエア先サービス側の内容抽出ロジックに任せる
Twitterにリンクを投稿すると、タイムライン上の表示にリンク先のサムネイルが提供される
twitter:descriptionに関してはTwitterカード上に概要がなにも入っていないとバランスが悪いので定型文を入れる
また、Facebookに投稿したときにニュースフィードに大きなサムネイルを表示する
前提 ユーザー画像付き投稿情報が登録されている
もし 投稿ページを表示している
ならば 以下の項目が要素に付加されていること
| 項目 |
| meta(ページの概要) |
| openGraph(ページタイトル) |
| openGraph(ページの種類:article) |
| openGraph(ページのurl) |
| openGraph(投稿のサムネイル画像url) |
| openGraph(サイト名) |
| openGraph(ページの概要) |
| twitterMeta(ページタイトル) |
| twitterMeta(ページの概要) |
| twitterMeta(cardタイプ:summary_large_image) |
| twitterMeta(サイトオーナーのtwitterユーザー名) |
どうでしょうか。結構どういうことを考えて対応しているかが分かるかと思います。基本的にstepに関しては開発者がきちんとメンテナンスしてくれて正しいと信用することが重要かと思います。そして、何故こういう振る舞いにしようとしたのか、どうしたいのか等振る舞いの意図するところなどは積極的にシナリオの下の概要に書いておくのが良いかと思います。featureファイルを読むだけでシステム内外がかなり分かる様になりそうですね(^^)
Ruby on RailsにTurnipを導入する
それでは具体的にサンプルRailsアプリにTurnipを導入してみてどういう感じになるか見てみましょう。
GherkinのススメなのでCucumberでも良いのですが、RSpecを利用したい場合はspecフォルダとfeaturesフォルダがrailsルートフォルダ直下に複数並んでコマンドも別(rails spec/rails cucumber)となってしまうので、RSpecを採用するつもりか、既に採用しているところにGherkinを導入してみようと考えるならTurnipの方がrspecでまとめて実行出来て楽です。
今回作成したサンプルはtoshi3221/turnip-rails-sample: Rails + Turnip導入サンプルで公開したので参考にしてください。
まずRubyは入っている前提からスタートします。サンプルを動作確認した環境は:
- macOS 10.13.2
- Ruby 2.5.0
- bundler 1.16.1
- Rails 5.1.4
- Turnip 3.0.0
です。まずRailsをインストールしてturnip-rails-sampleプロジェクトを作ります:
$ gem install bundler
$ gem install rails
$ rails new turnip-rails-sample
$ cd turnip-rails-sample
次はRSpecとTurnipをインストールします。Gemfileに2つのgemをテストで利用出来るように追加します:
group :development, :test do
gem 'rspec-rails'
gem 'turnip'
end
RSpecが利用出来るようにgemとプロジェクトにインストールします:
$ bundler install
$ spring stop
$ rails g rspec:install
springは直接関係ないのですがrspec:installで良く固まるので一旦止めて自動起動させてます。投稿リソースをCRUD出来るようにScaffoldを利用して機能追加します:
$ rails g scaffold post title:string body:string
この段階でRSpecのテストケースも生成されるので参考に出来ます。今回はTurnipのテストとして投稿画面一覧のIntegrationテストを作成するので、比較対象のRSpecテストも作成してみましょう。spec/requests/posts_spec.rbが生成されているので今回の趣旨っぽく編集します:
require 'rails_helper'
RSpec.describe "投稿", type: :request do
describe "一覧画面を開く" do
before { get posts_path }
it("正しく表示出来る") { expect(response).to have_http_status(200) }
end
end
実行してみます:
$ rspec --format documentation spec/requests/posts_spec.rb
投稿
一覧画面を開く
正しく表示出来る
Finished in 0.27386 seconds (files took 2.17 seconds to load)
1 example, 0 failures
こちらのテストケースをTurnipに移行します。turnipもRSpecのテストに追加します。.rspecに以下のturnip/rspecの読み込みと先ほどのdocumentationフォーマット指定オプションを追加します:
--format documentation
--require turnip/rspec
これでrspec実行でfeatureファイルもテスト対象として実行されます。spec/featuresにfeatureファイルを、spec/stepsにstepファイルを追加します。stepファイルをrspecテスト時にロードされるようにspec/spec_helper.rbに以下を追加しておきます:
Dir.glob("spec/**/*steps.rb") { |f| load f, true }
本題のturnipテストケースを追加します。以下の様にspec/features/posts.featureとspec/steps/turnip_rails_sample_steps.rbを記述することで先ほどのspec/requests/posts_spec.rbと同等のテストケースになります:
spec/features/posts.feature
# language: ja
機能: 投稿
シナリオ: 一覧画面が正しく表示出来る
もし 投稿一覧ページを表示する
ならば 正しく表示出来る
spec/steps/turnip_rails_sample_steps.rb
require 'rails_helper'
step '投稿一覧ページを表示する' do
visit '/posts'
end
step '正しく表示出来る' do
expect(page).to have_http_status(200)
end
featureファイルを実行します:
$ rspec spec/features/posts.feature
投稿
一覧画面が正しく表示出来る
もし投稿一覧ページを表示する -> ならば正しく表示出来る
Finished in 0.17225 seconds (files took 2.34 seconds to load)
1 example, 0 failures
無事、TurnipによるIntegrationテストをRailsで導入する事が出来ました!
ところで・・・巷ではCucumber+SeleniumのようなE2Eで使われてない?
既にCucumberやTurnipを利用している方からはGherkinって受け入れテストやE2Eテストで使ってるけど?って言われそうな気がします。が、自分が利用しているプロジェクトではrack_test+capybara+cucumberでnon-JSのインテグレーションテストのみで比較的安定品質でリリースが可能としています(HTML+jQueryというオーソドックスなWebアプリのせいもあります)。その辺のテストフェーズに関する体験を交えたレビューについては次回の記事にてまとめてみたいと思います。
まとめ
もう一度Gherkin導入のメリットをまとめておきます:
- 自然言語で書ける。英語でも日本語でもOK
- テスト仕様部分とロジック部分をfeatureとstepで分離しており何がテストされているか分かり易い
プロダクトの規模が大きくなったり期間が長くなってくるとメンバーが変化したり全体を把握しにくくなったりすることがありますのでそういうときこそ振る舞いや背景は何かという記述が1箇所にかつ軽量にまとまっている事は重要になってくると思います。その時の1アイデアとしてGherkinが選択肢として加わると幸いです。
良いGherkinライフを:)