ラベル 自動化テスト の投稿を表示しています。 すべての投稿を表示
ラベル 自動化テスト の投稿を表示しています。 すべての投稿を表示

2014年7月9日水曜日

capybaraのpage.execute_scriptとjQueryの組み合わせの汎用性がすごい

Webアプリ開発のテストに便利な Capybara(+Cucumber) ですが、テストに使うdriver がJavascriptに対応していれば、テスト中にJavascriptの実行を含めてブラウザの動きをシミュレートできるという素晴らしい特長があります。Javascript対応のdriverとしては、Capybara-WebkitやPoltergeistなんかが有名です。

ボタンやリンクのクリックなどのユーザの動作で発火するイベントは、Capybaraでclickなどのメソッドで指定すれば、Javascript側のイベントの動作もそのままシミュレートしてくれるようですが、マウスオーバーなどのイベントはどうやったらテストできるのでしょうか。

実は、Capybaraには execute_script メソッドがありまして、これでJavascriptのコード片を直接記述して実行できます。この中ではページで読み込んだライブラリなどが有効です。なのでjQueryが使えます。よってjQueryを使ってイベントを発火させることにより、かなりの汎用性を持ちえます。

例えば、マウスオーバーのステップ定義はこんなふうに書けてしまいます。

もし(/^".*?\((.*?)\)" にマウスポインタを乗せる$/) do |t_css|
  page.execute_script("$('#{t_css}').trigger('mouseenter')")
end

上記ステップ定義に従い、featureファイルにはこんなふうに書けますね。

もし "メニュー1(#menu1)" にマウスポインタを乗せる

カッコ内のCSSセレクタで指定した要素にmouseenterイベントが発生し、マウスオーバーの動作テストを実行することができます。

という感じで、execute_script メソッド内でjQueryを使うと簡単な記述で様々なことが実現出来そうです。特にテスト実行上重要なイベントの発火が非常に簡潔に書けるのでいいですね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月18日水曜日

自動テストのためのid属性とclass属性の使い分け

HTMLのid属性とclass属性の使い分けについては、「id属性はHTML文書内でユニーク(重複しない)であり1つの要素に1つのみ指定、class属性は重複可能で1つの要素に複数使用できる」という特性はあるものの、以前は使い分けは結構曖昧でした。

が、最近になって以下のように使い分けるようになりました。

  • id属性 → 自動化テストの要素特定のために使う。
  • class属性 → CSSによるスタイルの指定に使う。

理由を説明します。

id属性は単一指定だがclass属性は複数指定可能という特性から、自動テストという目的にはid属性のほうが向いています。複数指定だと、意図しない要素が指定されてしまったりする可能性もあり、テスト記述に余計な気を使うことになります。よって、勘違いやミスによるバグの発生はid属性のほうが少なくなると考えられます。

逆にスタイルの指定という観点からすれば、複数指定で重ねがけできるclass属性は都合がよく、CSSに使うのはclass属性でしょう。クラスの名前を工夫すればid属性は使わなくても済みますから、テストコードとデザインを分離することが可能です。

ちなみに動作速度はid属性指定のほうが速いらしいですが、どちらもブラウザの体感速度としては無視できるくらいに速いので、ユーザビリティの観点からは実質的に差はないです。しかし、自動テストでは幾度と無く表示を高速に繰り返すので、id属性指定の効果がある・・・かもしれません。「かもしれません」と書いたのは、実際にまだそこまで大量のテストを書いて試していないからです。まあ、動作速度としてはそのくらい差はわずかでしかないです。

上記の書き方に従えば、一つの要素に同じ値のid属性とclass属性を付ける場合も多いです。でもこれで自動テストとスタイリングの分離ができると考えれば、そんなに大した話ではないと思ってます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月13日金曜日

RSpecの"-fs"オプションが無効になったことへの対策

Padrinoのプロジェクトにおいてrake 経由でRSpecのテストを実行しようとして、今まで起きていなかったエラーが突然起きました。

`find_formatter': Formatter 's' unknown - maybe you meant 'documentation' or 'progress'?. (ArgumentError)

スタックトレースを見てもソースコードの原因箇所が的確に示されていなかったので少々戸惑いましたが、結局原因はspec/spec.rakeファイルにありました。そこではspecコマンドのオプションとして"-fs —color"が指定されていたんですが、実はこの"-fs"が原因でした。"-fs"は"--format specdoc"の短縮形式で、以前はこのオプションは有効だったが無効になったみたいです。

現在はフォーマットとして指定できるのは"documentation"か"progress"になっていますが、詳細な情報を出力したいので"documentation"を指定することにします。この指定にあわせてspec/spec.rakeファイルを修正しようと思います。

Padrinoが自動生成する修正前のspec/spec.rakeファイルはこんな感じです。

begin
  require 'rspec/core/rake_task'

spec_tasks = Dir['spec/*/'].inject([]) do |result, d|
 result << File.basename(d) unless Dir["#{d}*"].empty?
 result
end

  spec_tasks.each do |folder|
    RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
      t.pattern = "./spec/#{folder}/**/*_spec.rb"
      t.rspec_opts = %w(-fs --color)
    end
  end

  desc "Run complete application spec suite"
  task 'spec' => spec_tasks.map { |f| "spec:#{f}" }
rescue LoadError
  puts "RSpec is not part of this bundle, skip specs."
end

これを以下のように変更します。

begin
  require 'rspec/core/rake_task'

  spec_tasks = Dir['spec/*/'].inject([]) do |result, d|
    result << File.basename(d) unless Dir["#{d}*"].empty?
    result
  end

  spec_tasks.each do |folder|
    desc "Run spec suite under #{folder} directory"
    RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
      t.pattern = "./spec/#{folder}/**/*_spec.rb"
      t.rspec_opts = %w(--format documentation --color)
      t.verbose = false
    end
  end

  desc "Run complete application spec suite"
  RSpec::Core::RakeTask.new("spec") do |t|
    t.pattern = "./spec/**/*_spec.rb"
    t.rspec_opts = %w(--format documentation --color)
    t.verbose = false
  end
rescue LoadError
  puts "RSpec is not part of this bundle, skip specs."
end

変更点としては、-fsオプションをやめて--format documentationにしたこと、
t.verbose = falseを追加したこと、"spec"タスクを"spec:フォルダ"の集合ではなく
新たに全specファイルを含むタスクとして定義したことです。

t.verbose = falseを指定するのは、長ったらしい実行コマンドの出力を省いて出力を見やすくするためです。また、"spec"タスクが"spec:フォルダ"の集合だと、何件成功して何件失敗したか、という結果がフォルダの実行ごとに表示されるので見づらくなるので、対象を全てにした1つのテストスイートにまとめなおしています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月6日金曜日

Capybaraで入力欄・ボタン・リンクをCSSセレクタによって指定する

CapybaraはWebアプリケーションをテストする際に便利なライブラリです。ところで、フォームに入力するメソッドとしてはfill_inがありますが、要素の指定にCSSセレクタが使えません。click_button, click_linkも同様にCSSセレクタが使えません。ではXPathは使えるかというと、XPathも使えません。指定できるのは単なる文字列で、例えばfill_inの場合、その文字列がid属性またはname属性または対応するlabel要素の文字列に合致するものを探すことになっています。他のfindなどの要素指定メソッドではCSSセレクタを使うので、他のメソッドとの統一性がなくて分かりにくいし、細かい指定もできないので、CSSセレクタを使いたいところです。

単純な解決策としては、findメソッドで要素を絞り込んでsetやclickすればいいわけですが、それだけだともとの機能と同一にはになりません。というのは、fill_in, click_button, click_linkの特長として、それぞれのタイプに合致する要素だけを探してくれるのです。例えばfill_inならば入力可能フィールドのみを検索対象としてくれます。

これは、Capybaraのこのソースコードを見るとよくわかります。

例えばfill_inの場合、"fillable_field"という条件で探してsetしています。これと同じことをできればいいわけです。条件の詳細はXPathライブラリのこのソースコードを見るとわかります。

これによるとfill_inに使われる"fillable_field"の条件は、

  • input要素あるいはtextarea要素であり、
  • type属性が次のいずれでも「ない」:'submit', 'image', 'radio', 'checkbox', 'hidden', ‘file'

click_buttonに使われる"button"の条件は、

  • input要素かつ、type属性が次のいずれかである:'submit', 'reset', 'image', ‘button'
  • または、button要素である

click_linkに使われる"link"の条件は、

  • a要素かつ、href属性が存在する

となっています。これらの条件を満たすように、CSSセレクタに条件を付加してXPathに変換するメソッドを作ってみました。

# CSSセレクタをXPathに変換し、さらに入力可能フィールドの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_fillable_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  # init_xpath = %(#{base_xpath}[../input or ../textarea])
  # new_xpath = ['submit', 'image', 'radio', 'checkbox', 'hidden', 'file'].inject(init_xpath) do |tmp_xpath, attr|
  #   %(#{tmp_xpath}[not(@type='#{attr}')])
  # end
  # new_xpath
  # 高速化のためベタ書き
  "#{base_xpath}[../input or ../textarea][not(@type='submit')][not(@type='image')][not(@type='radio')][not(@type='checkbox')][not(@type='hidden')][not(@type='file')]"
end

# CSSセレクタをXPathに変換し、さらにボタンの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_button_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  # xpath_input = %(#{base_xpath}[../input][@type='#{['submit', 'reset', 'image', 'button'].join("' or @type='")}'])
  # xpath_button = %(#{base_xpath}[../button])
  # "(#{xpath_input} | #{xpath_button})”
  # 高速化のためベタ書き
  "(#{base_xpath}[../input][@type='submit' or @type='reset' or @type='image' or @type='button'] | #{base_xpath}[../button])"
end

# CSSセレクタをXPathに変換し、さらにリンクの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_link_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  "#{base_xpath}[../a][@href]"
end

少々解説します。Nokogiri::CSS.xpath_for は、複数条件CSSに対応するため、返り値はXPath文字列を要素に持つ配列になります。入力欄を選ぶという状況的に、1つのみになると考えられるので最初の要素を選択すればよいから、Nokogiri::CSS.xpath_for(css)[0]としています。

変換したXPathの後ろに例えば [../input or ../textarea] といった要素指定を付加しています。これは、要素自身がinput要素あるいはtextarea要素であることを後から無理やり指定するためです。スマートではない形式ですが、CSSセレクタを変換して条件を付加するという状況なので仕方ありません。

最初はメソッド中のコメントアウトしてある部分のようにかなり動的にXPathを生成していましたが、テスト実行を少しでも早くするために、文字列をベタ書きしておくことにしました。

さて、使い方としてはCSSセレクタを渡してXPathに変換し、その条件でノードを取得して動作を行えばよいです。Cucumberによる例を挙げると、こんな感じでステップ定義に使えます。何やら余計なものが色々付いているように見えますが、その辺の詳細は以前に書いたこの記事を参照。

もし(/^".*?\((.*?)\)" 欄に "(.*?)" と入力(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_with, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_fillable_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).set(t_with)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" ボタンを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_button_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" リンクを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_link_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
  puts '' if t_eos.blank?
end

Gerkinによるfeature定義としては、例えば

もし "アカウント名(#account_name)" 欄に “foo" と入力し、
かつ "パスワード(#password)" 欄に “bar" と入力し、
かつ "ログイン(#login-button)" ボタンを押下した
ならば・・・(以下略)

のようになります。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月4日水曜日

Capybaraでテスト実行中にセッションIDを取得する

テストにCapybaraを使ってるんですが、セッションIDが変化するかどうかをテストしたいと思いまして。ところが、セッションの中身を見る場合は rack_session_access っていうライブラリが使えるんですが、rack_session_accessではセッションID自体を取得する方法は提供してくれません。

Rack::Session::Cookieを使っている場合にはセッションの中身にセッションIDが入っているので、rack_session_access でセッションIDを取得できますが、Rack::Session::Cookie以外によるセッション機能を使っている場合はrack_session_accessではセッションIDが取得できません。

何とかできないものかと思案し、発想を転換しました。Rack::Session::Cookie以外によるセッション機能を使っている場合、ブラウザのCookieにはセッションIDそのものが入っているわけです。つまり、Cookie経由でセッションIDが取得できるはずです。

ということで調べると、Capybaraのドライバを呼び出して、ドライバ内部のCookieを取得するメソッドを呼び出せばいいみたいです。なお、ドライバ内部のCookieを取得するメソッドの呼び出し方法は、ドライバごとに異なります。次の例は、ドライバがPoltergeist の場合の例です。

@session_id =  page.driver.browser.cookies['rack.session'].value


などとすれば取得できます。

なお、セッションIDが変化するかどうかをテストしたい場合はセッションIDを保存しておく必要がありますが、単純にインスタンス変数に入れておけば、後のステップでそのままインスタンス変数として呼び出すことができます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月3日火曜日

RSpecで外部コマンド実行結果を判定する

RSpecによる自動テスト中に、外部コマンドを実行して成功したかどうかを判定したいとします。そういう時は、実行ステータスを取得でき、なおかつ外部コマンドの終了を待ち合わせるような外部コマンドの実行方法を採用すればいいでしょう。

Rubyには外部コマンドの実行方法がいくつか存在しますが、Open3.capture2あたりが適しているように思います。コマンドの標準出力への出力結果はテストで使ったりするため取得したいことが多く、なおかつ標準エラー出力はそのままコンソール上に表示することでテスト中に外部コマンドでエラーが起きた際に原因が分かりやすくなります。

コマンドの実行が成功したかどうかを終了ステータスで判定すれば、エラーが起きた際に分かりやすくなるでしょう。コード例を挙げておきます。

stdout, status = Open3.capture2 %(some_command), :stdin_data => stdin_string
expect(status.exitstatus).to eq(0)

上記コードでは終了ステータスが0、つまりコマンドが成功することを期待しています。コマンドが異常終了して終了ステータスに0以外が返ると、テストは失敗となります。

なお、僕の場合実際には上記コードをRSpecから直接実行するのではなく、Cucumberのステップ定義に組み込んで使っています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月30日金曜日

capybara-webkitからpoltergeist(phantomjs)に乗り換え

RubyによるWebアプリケーションの自動化テストに便利なCapybara。JavascriptによるDOM操作も、capybara-webkitドライバを経由してテストすることができます。当初はそれで良かったんですが、とある事情によりXサーバ関連ファイル群を入れないで動かしたい場合があったので、Xサーバ関連ファイル群を必要とする仮想フレームバッファ(xvfb)への依存を避けたくなりました。

Capybaraにはもう1つ、JavascriptによるDOM操作を含むテストを実行できる手段がありまして、それがPoltergeistドライバを経由してPhantomJSを動かす、というものです。PhantomJSは仮想ブラウザみたいなもので(乱暴な説明)、詳しい説明は他サイトに譲ります。CabybaraからPhantomJSを動かすためのドライバがPoltergeistです。

ということで、Capybara-webkitからPoltergeist(経由のPhantomJS)に乗り換えることにしました。

まず、PhantomJSをインストールします。僕の環境としてはDebian7ですが、公式サイトのビルド済みパッケージを取ってきたらそのまま動公式サイトにLinux用のビルド済みバイナリがあるので、入手してください。2014/05/29現在の最新版はver1.9.7であり、
"phantomjs-1.9.7-linux-x86_64.tar.bz2"というリンクからダウンロードできます。

解凍して出来たディレクトリの bin/phantomjs をPATHの通った適当な場所に移動します。

例)$HOME/bin に移動する場合

[user@host]% tar jxvf phantomjs-1.9.7-linux-x86_64.tar.bz2
[user@host]% cp phantomjs-1.9.7-linux-x86_64/bin/phantomjs ~/bin

なお、phantomjsの動作には fontconfig, freetype ライブラリが必要ですが、最近のLinuxデスクトップ環境ならば、ほとんどの場合にはすでに入っているでしょう。

ruby側の対応は巷のサイトに書いてあるとおりでOKです。まずGemfileへPolgergeistの記述を追加します。

gem 'poltergeist'

僕はCucumber経由でcapybaraを使ってますので、features/support/env.rb に必要ライブラリの require と default_driver, javascript_driverの変更を記述します。(Javascirpt関連テストの時だけ使いたい場合は、default_driver側の記述は不要です。)

require 'capybara/poltergeist'
Capybara.default_driver = :poltergeist
Capybara.javascript_driver = :poltergeist

なお、Cucumberのテスト自体は書き換えずに動作しました。まだそんなにテストの量がないせいか、速度の違いは感じられません。

PhantomJSは活発に開発されているようなので、今後も色々と改善が期待できますね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月17日土曜日

Cucumber(+Capybara)ステップ定義の実用的サンプル

現在のプロジェクトでは初めてBDDを採用して開発してます。具体的にはCucumberを使ってステップ定義を書いています。

Cucumberについて調べると、確かにサンプルとしてGherkinによるステップ定義の例は色々出てくるんですけれども、日本語によるステップ定義で実用的というかそのまま実務に使えるサンプルってネット上に出ているものが少ない気がします。実務的じゃないものは沢山見つかるんですけどね。

ということで、BDDはまだ初心者といえる私ですけれども、実務ですぐに使える日本語で書いたGherkinによるステップ定義のサンプルを晒してみたいと思います。なお、具体的なテストコードの記述にはCapybaraを利用しています。

ちょっと長いですが、実用的なサンプルということでご理解ください。ではどうぞ。

もし(/^".*?\((.*?)\)" 画面にアクセス(?:し(、)|した)( debug)?$/) do |t_path, t_eos, debug|
  show_debug_info if debug
  visit t_path
  puts '' if t_eos.blank?
end

もし(/^HTTPステータスコードが "(\d{3})" で(?:あり(、)|ある)( debug)?$/) do |t_code, t_eos, debug|
  show_debug_info if debug
  expect(page.status_code).to eq(t_code.to_i)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" 欄に "(.*?)" と入力(?:し(、)|した)( debug)?$/) do |t_locator, t_with, t_eos, debug|
  show_debug_info if debug
  page.fill_in(t_locator, :with => t_with)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" ボタンを押下(?:し(、)|した)( debug)?$/) do |t_locator, t_eos, debug|
  show_debug_info if debug
  page.click_button(t_locator)
  puts '' if t_eos.blank?
end

ならば(/^ページタイトルが "(.*?)" で(?:あり(、)|あること)( debug)?$/) do |t_title, t_eos, debug|
  show_debug_info if debug
  expect(page.title).to eq(t_title)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" が存在(?:し(、)|すること)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  expect(page).to have_css(t_css, :wait => t_async ? async_time : 0)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" が存在(?:せず(、)|しないこと)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  expect(page).to have_no_css(t_css, :wait => t_async ? async_time : 0)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" 画面に遷移(?:し(、)|すること)( debug)?$/) do |t_path, t_eos, debug|
  show_debug_info if debug
  expect(page.current_path).to eq(t_path)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" に "(.*?)" と表示(?:され(、)|されていること)(\(非同期\))?( debug)?$/) do |t_css, t_text, t_eos, t_async, debug|
  show_debug_info if debug
  target_node = page.find(t_css, :text => t_text, :visible => true, :wait => t_async ? async_time : 0)
  expect(target_node.text).to eq(t_text)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" に "(.*?)" と表示(?:されず(、)|されていないこと)(\(非同期\))?( debug)?$/) do |t_css, t_text, t_eos, t_async, debug|
  show_debug_info if debug
  target_node = page.find(t_css, :text => t_text, :visible => true, :wait => t_async ? async_time : 0)
  expect(target_node.text).not_to eq(t_text)
  puts '' if t_eos.blank?
end

def async_time
  Capybara.default_wait_time
end

def show_debug_info
  puts 'Respnse Headers: ' + page.response_headers.to_s
  puts 'Status Code: ' + page.status_code.to_s
  puts 'Current Url: ' + page.current_url
  puts "HTML:\n" + page.html
end

以上になります。いくつかポイントを解説します。

URLやボタンや入力欄などの条件はカッコ内に記述するようにしているものがあります。例えば画面へのアクセスは "画面名(URL)" という記述です。他にも "ボタン名(HTMLのID)" とか、 "探したい要素の呼び名(CSSセレクタ)" とかですね。これはテストの見た目のわかりやすさと同時に、ステップ定義を使いまわせることを実現するためです。

各ステップは末尾に " debug" (半角スペースの後ろにdebug) とつけると、そのステップ実行直前のレスポンスについて、レスポンスヘッダ、ステータスコード、URL、HTMLコードをデバッグ情報として出力します。テストをしながら失敗したステップの時の状態が知りたいことってあると思うので、こうしてステップ定義にデバッグ情報の出力オプションを付けておけば便利ですよね。

あと、末尾に "(非同期)" とつける事のできるステップもあります。これは、Capybaraの機能で、例えばレスポンス中に指定した要素が存在するか確認するコードの場合、見つからなくても非同期処理の可能性を考慮して一定時間待ってから再度探してくれる、という機能を切り替えるためのものです。Capybaraではデフォルトで毎回2秒待ってくれるのですが、非同期を想定していない場合、この待ち時間は無駄です。テスト駆動をする場合はテスト失敗を頻繁に起こすのでなおさら無駄な待ち時間は無くしたいです。ということで、末尾に "(非同期)" と付けない場合はCapybaraの待ち時間を0秒に設定することで、無駄な待ち時間が発生しないようにしています。

あと、細かい点になりますが、ステップ定義文の最後の語尾に読点(、)をつける書き方とそうでない書き方の2通りできるようにしてまして、後者(読点なし)の場合には、次の行に空行を出力するようにしています。これは、Cucumberの実行時に、ステップ定義のソースコード上での空白行を無視して出力してしまって見辛いので、強制的に空行を再現させる目的で付けています。言葉で説明するとわかりにくいと思いますが実行結果を見れば一発でわかると思います。こんな風になります。


空行が出力されるので、結果がぐっと見やすくなりますね。

もしかしたら今後開発を進めるうちにまた色々変わってきたらアップデート版を書くかもしれませんし、書かないかもしれませんが、参考になれば幸いです。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月3日土曜日

テスト駆動開発の自動テスト"以外"の部分

それにしても、テスト駆動開発を実際にやっていると、テスト駆動開発っていうのは自動化テストありきの話では全くないということに気づきます。つまり、自動化テストが全くないとしても、テスト駆動開発的な進め方・考え方というのは元からあったわけで、それが自動化テストという仕組みにぴったりマッチした、と捉えることもできるのですよ。

例えば、テスト駆動開発の基本的な流れは、
  1. テスト記述
  2. テスト実行(レッド)
  3. コード記述
  4. テスト実行(グリーン)
  5. リファクタリング
  6. テスト実行(グリーン)
  7. (1. に戻る)
と繰り返していきますよね。

でもこれ、例えばWebアプリ開発で言えば、自動化テストの代わりに、ブラウザ上で実際に手動で動かして確認することに置き換えると、実は昔からやっていたやり方になるんですよね。要するに、
  1. 実現したい動作を決める
  2. ブラウザで現在の動作を確認する(失敗)
  3. コード記述
  4. ブラウザで現在の動作を確認する(成功)
  5. リファクタリング
  6. ブラウザで現在の動作を確認する(成功)
  7. (1. に戻る)
という感じですね。リズムとしては、ブラウザによる動作確認が自動化テストに置き換わればそのままテスト駆動開発になってしまいます。ですので、テスト駆動開発を始めた時に、全く違和感なく実に自然に始めることができたんですよね。
多分この自動テスト"以外"の部分が以前からの開発の流れとぴったり重なると、テスト駆動開発を容易に採り入れることができ、恩恵を享受できると思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年4月25日金曜日

仕様書・自動化テスト・実装の三位一体開発

タイトルは最近考えてることでしてね。ちなみに最初に申し上げておくべきことは、以下に書くことは、仕様を決める権利・スケジュールを決める権利・開発方法を決める権利を自らが持っている開発チーム及び開発者を対象としている、ということです。ということは受託開発よりも、自社サービスや自社製品の開発あるいは社内システム開発という立場向け、ってことです。

言いたいことを一言で言うと、仕様書・自動化テスト・実装を行き来しながら少しずつ作っていく、ということです。例えば、仕様書に少し書き加え、該当する自動化テストを少し書き、さらに該当する実装を少し書く。それを小さいサイクルで繰り返していく。というふうに。

もちろん、例えば実装方法に依存して考えなければならない部分があったりして、先に仕様書が書けない場合なんかもありますけど、その時は実装を先に書いてもいいわけです。その代わり可能な限り早く仕様書に反映し、該当する自動化テストも書くようにします。つまり、どれか1つだけが大きく突出しないようにします。

これの一部だけを取り出すと、自動化テストと実装の部分だけ見れば「テスト駆動開発」になるでしょう。さらに自動化テストの範囲が広がって振る舞いや挙動の記述も含むようになれば「ビヘイビア駆動開発」となりますね。

もし仕様書だけを集中して先に完成させてしまうと、いわゆるウォーターフォール型になります。あるいは仕様書も一緒に作っていくという概念を抜かせばアジャイルになります(大雑把に言えば)。

なお、「◯◯駆動開発」と呼んでしまうと、その「◯◯」を原則として先に作らなければならないということになりますが、仕様書・自動化テスト・実装は常に順番を固定して作れるようなものではなく、むしろお互いを行き来しながらバランスを取りつつ進めていく、という表現がしっくりきます。その上で、理想としては「仕様書→自動化テスト→実装」の順番のサイクルを維持する、と思っておけばいいと思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年4月1日火曜日

Cucumberで各シナリオのテスト実行前にDBリセット

Cucumberでテストを実行しようとするときに、各シナリオ実行前にデータベースをテスト用の初期状態にセットしておきたいって思ったんですよ。各シナリオ内部で書いてもいいのかもしれませんが、毎回同じ状態にセットするのも面倒ではあります。

そこで、フックを利用します。support/env.rb にフック登録用メソッドで処理を定義すると、決められたタイミングで実行してくれます。今回はシナリオ実行前なので、Beforeメソッドを使います。

さらに、DBリセット時のログをターミナル上に出してしまうと、テスト実行結果の出力と混じって見づらくなってしまいます。そこで、DBリセット時はログをファイルに吐き出すことにします。

Padrinoフレームワーク上で、ActiveRecordを使っている時のsupport/env.rb の一例を書いておきます。

# encoding: utf-8
RACK_ENV = 'test' unless defined?(RACK_ENV)
require File.expand_path(File.dirname(__FILE__) + "/../../config/boot")

require 'capybara/cucumber'
require 'rspec/expectations'
require 'thor'

##
# You can handle all padrino applications using instead:
#   Padrino.application
Capybara.app = ProjectName::App.tap { |app|  }

# ログファイル用のディレクトリ・ファイルの準備
log_dir = Padrino.root('log', 'cucumber')
log_path = log_dir + '/before.log'
FileUtils.mkdir_p(log_dir) unless FileTest.exist?(log_dir)
File.open(log_path, 'w').close

# 標準出力をバックアップ
stdout_old = $stdout.dup
# テーブル一覧を取得(テーブル名からモデルクラス名を求めて定数化)
tables = (ActiveRecord::Base.connection.tables - ['schema_migrations']).map { |t| t.classify.constantize }
# データファイル
seed_file = Padrino.root('db', 'seeds.rb')
# フック実行済み回数
hook_done_count = 0

# 各シナリオの実行前に起動するフック
Before do |scenario|
  # 標準出力をログファイルに吐き出す
  $stdout.reopen(log_path, 'a')
  $stdout.write("\n") if hook_done_count > 0
  $stdout.write(Time.now.instance_eval { '%s.%03d'%[strftime('%Y/%m/%d %H:%M:%S'), (usec / 1000.0).round] } + "\n")
  $stdout.write('機能: ' + scenario.feature.name.split("\n").first + "\n")
  $stdout.write('シナリオ: ' + scenario.name.split("\n").first + "\n")
  $stdout.write("\n")

  # データベース初期化
  tables.each do |tbl|
    tbl.destroy_all
  end
  load(seed_file) if File.exist?(seed_file)

  # 標準出力を元に戻す
  $stdout.reopen(stdout_old)

  hook_done_count += 1
end

def shell
  @_shell ||= Thor::Base.shell.new
end

補足すると、seeds.rbファイルをロードすることにより、テストの初期状態データセットをロードすることになっていますが、その中でThorを使い、shellという名前でThorオブジェクトを扱っているので、Thorオブジェクトを返すshellメソッドを定義しています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。