続・Casper.JSのススメ

前回: Casper.JSのススメ
9/18追記: JSの読み込みと実行を待たせるためにcasper.waitを使う旨を追記

CasperJSで受け入れテスト書くかって話になったので、チームの皆さんにおはなしした。
↓少し削っているのでちぐはぐな気がする。

あとは口頭での説明だったので、その内容をざっくりまとめる。

①casperjsコマンド

$ casperjs test test/suite/ --direct --log-level=debug --fail-fast --pre=test/common/casper_pre.coffee --includes=test/common/casper_inc.coffee --xunit=log.xml

testサブコマンドが必要。オプションは以下のとおり。

  • --pre: 全ファイルの実行前に一度だけ実行されるスクリプトを指定。--postだと全ファイルの実行後となる
  • --includes: 各ファイルの実行前に毎回実行されるスクリプトを指定
  • --direct: 標準出力に直接出力する
  • --fail-fast: テストケースがひとつでもfailした時点で実行を終了する
  • --log-level: 出力する情報を選べる。error, warning, info, debugから一つ指定し、debugが最も情報量が多い
  • --xunit: テスト実行ディレクトリに、テスト結果のXMLファイルを出力する。Jenkinsに渡して利用

②よく使うメソッド

casper.test.comment 'コメントです'
  • 標準出力に引数の内容のコメントを出力させられる
casper.test.begin 'テスト名', ->
  テスト内容
  • テストを開始する
  • 第一引数はテスト名(開始時にコメント同様出力される)、第二引数は開始時の処理内容のクロージャ
casper.then ->
  処理内容
  • 処理内容を区切るブロック
  • 引数に渡したクロージャの処理を行う
  • 記述する単位は、casper.clickまでが良さそう(複数の遷移を含ませるとうまくいかないようだ)
  • なので、前ブロックからの遷移を確認するテストメソッドから始めて、クリックして画面遷移が発生するまでを一つのcasper.thenに渡せば良さそう
  • (9/18追記)CKEditorなどJSの読み込み完了を待ってはくれない。そのため、casper.wait('1000')とかでディレイを入れる必要があるようだ
  • (9/18追記)じゃあ何を待ってくれるんだよって話はこちら
casper.thenOpen '開くURL', ->
  その後の処理
  • 特定のURLを開く
  • 類似メソッドがあるようなので適宜調べる
casper.fill 'フォームが含むタグ名#id',
  'input要素のname': '入力内容'
  • フォームに文字を入力する
  • 第一引数が入力するフォームのあるタグ名、第二引数がinput要素のname, オプションの第三引数ではtrue or falseを渡すとそのままsubmitしてくれるかを選べる(デフォルトでfalse)
  • 後述のcasper.fillSelectorsのほうがselect要素などにも値を設定できるので、そちらを用いるよう統一したほうが良いかも
casper.fillSelectors 'フォームを含むタグ名#id', 
  'input[name="hoge"]': '入力内容とか値',
  'select[name="fuga"]': '入力内容とか値'
  • フォームやセレクトボックスなどに値をセットする
casper.test.assertTextExists '確認対象の文字列', '出力する内容'
  • body内で第一引数の文字列が存在するかをテスト
  • 第二引数の内容がラベルになる
casper.click 'input[name="confirm"][type="submit"]'
  • クリックする
casper.run ->
  casper.test.done()
casper.evaluate ->
  実行したいJavascript
  • 引数で渡したクロージャ内のJSを実行
  • (9/18追記)casper.thenと同様に、JSの実行完了をCasper.JSは待たない。casper.waitでディレイを入れるとうまくいく。
casper.echo casper.getCurrentUrl()
  • 現在のURLを出力
casper.withFrame 'フレームを特定するnameとか', ->
  iFrame内に実行したい処理
  • iframe内にmain的な何かを移して処理を行う
  • よくわかっていない

③ユニークで共通な値として時刻を使ったり、sessionを使いまわしたい
重複が許されない値があるとき、入力値として時刻を使いたいと思う。
こういう時は、

casper.echo "Get Time"
date = Date.now()
fs = require 'fs'
fs.write '/tmp/casper_date.txt', date, 'w'
casper.test.done()

上記のような処理を書いて --pre=pre.coffee で指定して最初に実行されるようにして、時刻を取得してユニークな値に使えば良いと思う。
また、sessionを使いまわしたい場合、

casper.test.comment 'ログインする'

casper.test.begin 'login', ->
  casper.start()
  casper.thenOpen 'http://localhost/login', ->
    casper.fill 'ul#login',
      'login_id': 'saisa'
      'password': 'hoge'
    casper.click 'input[type="submit"][name="login"]'

casper.then ->
  casper.test.assertTextExists 'saisa さん', 'login message is found'

casper.run ->
  fs = require 'fs'
  cookies = JSON.stringify @page.cookies
  fs.write '/tmp/casper_cookies.txt', cookies, 'w'
  casper.test.done()

このように、ログインしたタイミングでcookieをファイルに書き出せば、expireされない限りログイン状態を維持できる。
これらのファイルを読み込むには、

casper.echo 'load date.txt'
fs = require 'fs'

# 共通の時刻を取得
# 引数:ファイル名
# date -> 時刻を取得
# cookies -> adminログインのcookie
casper.loadDataFile = (fileName) ->
  if fs.exists "/tmp/casper_#{fileName}.txt"
    casper.echo "#{fileName} loaded"
    return fs.read "/tmp/casper_#{fileName}.txt"
  else
    casper.test.fail 'Cannot load #{fileName}.txt'

casper.test.done()

とinclude.coffeeに書いて、実行時に --includes=inc.coffee と指定すればよい。

casper.test.done()まで呼ばれて次のテストファイルが実行されるタイミングでcookieが破棄されるようなので、このようにしなければならない。
各テストファイルの実行前に --includes で指定されたファイルは読み込まれるため、各ファイルの先頭で独自に定義した読み込みメソッドを呼べば、cookieや時刻を同じ値で使いまわせる。

④調べ方
公式ドキュメントは言うまでもないが、そのほかにStackOverflowにも情報が多いという印象。
日本語の情報は少ないので、英語に臆しないことが重要だと思う。
それと、多くの文章でインスタンスを作りメソッドを叩いている記述が見られるが、複数ファイルを読みこませる場合はインスタンスを作れない。
この点に注意を要する。

⑤まとめ
とりあえずこれくらい調べれば、webアプリケーションに受け入れテストを書き始めることができるだろう。
だが本当に大変なのは、テストを維持管理していく運用フェーズだと思う。
エクセル管理では破綻が目に見えているので、根本的な対策を考えなければと思う。

このテストは、本来はアプリケーションが仕上がりつつあるタイミングで、QA的な立場から行うものなのだろう。
しかし、レガシーコードの改善に立ち向かう、その長い戦いの第一歩としても、有用だ。
なぜなら、値をHTTPで投げてHTMLを受け取ることができれば良いのだから。

日本語の情報が乏しいので、組織的かつ継続的に書いてみた・みたいという人がいたら、ぜひリプやコメントをしてほしい。