Capybaraでのテスト中にスクリーンショットを撮りまくって幸せになる。
業務でのスクリーンショットといえば、エクセルとあわせて語られるようなつらい話がセットになっている印象があります。
マニュアル撮影するのは本当につらそう……でも自動テストで自動で撮るスクリーンショットは最高です。 わたしは大好きです。
そのスクリーンショットを、テストついでのなんとなくみたいなノリで簡単に撮れるようにしておくといろいろ便利でしたので、どのようにやっているか書きます。
Capybara
とPoltergeist
を使用、PhantomJSのバージョンは2.0の環境下で書きました。
ところで、スクリーンショットが役に立った局面
前職では、責任をとるための上司と、名ばかりディレクター、そしてわたしという編成で仕事をすることがわりとありました。
必然的にアプリケーションの細かいところも全部わたしが直接やりとりしていましたが、基本的に先方も別のお仕事をなさっているので、はっきり言って開発中だとクリックさえもしてくれないわけです(まぁ納品直前もあまり積極的にクリックしてくれない、例外はあまりない)。
そんな時にスクリーンショットが大量にあると、画面はこういう流れです、文言はこうです、配置はこう、間違ってないか即座に返答してください早くしてくださいという話がとても早くなりました。
その前にCapybara
でテストしやすく作っておく
Capybara
で行うテストは、一般的に以下のような流れになると思います。
querySelector
なシンタックスなどを用いて要素を特定click
したりfill
したりする- 再び要素を特定しての
text
などの変化を調べる
各要素の特定が支点になるため、特定の容易さがテストのしやすさに直結します。
RailsのFormHelper
類を使用すると自動的にID
が振られますが、その他の要素にID
を逐一つけていくのも難儀であるので、HTMLのclass
で特定できるようにしておきます。
HTMLのclass
の有効活用
btn btn-lg btn-success
といったような、どのような表示をするかではなく、submit-button
などそのページ、その界隈での役割をセレクター名に使うことにより、要素の特定がとても楽になります。
個人的には管理も楽になるのでおすすめです。(静的サイト制作界隈においては、HTML側で表示を柔軟に組みあわせられたほうが便利なのかも知れませんね)
:link: プログラマが楽にCSSを書いて管理する。 - Qiita
スクリーンショットを楽に撮れるようにする
Capybara
にはsave_screenshot(path = nil, options = {})
というメソッドが用意されており、これを使うことによってスクリーンショットを撮ることができます。
このままでも十分有用ですが、スクリーンショットを有効活用するには目的に応じたpath
が必要なため、名前を考えたり、意外とめんどくさい感じがあります。
そこらへんをなるべく手間なく、撮りたい時に気軽に撮れるようにやっていきます。
RSpec::Core::Example
から名前の素をいただく
RSpec::Core::Example
はそのテストに関するメタ情報などが入るクラスです。
it
やscenario
のブロックに引数としてインスタンスが渡されますので、これを参照し、path
の元になりそうな要素を得ます。
feature 'ある局面' do
feature 'ある機能' do
scenario 'ある一連の確認' do |example|
example.description
# "ある一連の確認"
example.full_description
# "ある局面 ある機能 ある一連の確認"
end
end
end
full_description
を元にすれば、どのテストで撮られたスクリーンショットか簡単に命名ができますね。
基本となる名前はこれでよしとして、スクリーンショット画像ファイルを同名では複数保存はできないので、そこに対処します。
連番と、保存するディレクトリを決める
スクリーンショットの名前は、その状況を説明するために、テスト内で適切な文字列を渡すことになります。
ところで、Capybara
で行われるテストは一連の動作のテストですから、スクリーンショットがいつの時点で撮影されたかが重要になります。
文字列でなんとなく順番を確認したり、手動で番号を入れるのはテスト(とそのスクリーンショット)の追加削除にヨワいので、そこらへんを解決するクラスを用意します。ついでにスクリーンショットを保存するディレクトリの管理もします。
require 'pathname'
class ScreenShotMan
class << self
attr_accessor :dir
def ss_dir
raise 'Dir required' if dir.empty?
Pathname.new(dir)
end
def path_for_ss(*dirs)
ss_dir + dirs.flatten.join('/')
end
end
attr_accessor :dir, :count
def initialize(*descriptions)
self.dir = self.class.path_for_ss(*descriptions)
self.count = 0
end
def clean!
`find #{dir}/ -name '*.png' | xargs -r rm`
end
def filename!(name)
self.count += 1
"#{dir}/#{count}_#{name}.png"
end
end
ScreenShotMan.dir = "#{Rails.root}/log/ss"
こうしておけば、あとはテスト内でインスタンスを作成すれば適当なpath
がどんどん得られます。
ss_man = ScreenShotMan.new('あるテスト')
ss_man.filename!('任意の名前')
#=> "/home/mmmpa/rails_app/log/ss/あるテスト/1_任意の名前.png"
ss_man.filename!('さらなる名前')
#=> "/home/mmmpa/rails_app/log/ss/あるテスト/2_さらなる名前.png"
しかしこれをあらためてsave_screenshot
に渡すというのも手間感があるので、モンキーパッチしましょう。
スクリーンショットにまつわる色々を自動でやっていく
下記のモンキーパッチでは、先述のRSpec::Core::Example
からpath
を決定し、過去のスクリーンショットを削除して準備するready_ss
メソッドと、場合に応じていい感じ(個人的嗜好)にsave_screenshot
を行うtake_ss
定義しています。
module RSpec
module Core
class ExampleGroup
def ready_ss(ex, strict_height = nil)
@ss_man = ScreenShotMan.new(ex.full_description.split(' '))
@ss_man.clean!
@strict_height = strict_height
end
def take_ss(name, sleeping = 0)
sleep sleeping
height = begin
page.evaluate_script('document.querySelector("body").clientHeight')
rescue
1000
end
page.driver.resize(1240, @strict_height || height)
page.save_screenshot(@ss_man.filename!(name))
end
end
end
end
定義しただけなので、ここらへんだけはテストケースに漏れ出します。
feature 'ある局面' do
before :each do |example|
ready_ss(example)
end
feature 'ある機能' do
scenario 'ある一連の確認' do
take_ss('ハイチーズ')
end
end
end
これで/home/mmmpa/rails_app/log/ss/ある局面/ある機能/ある一連の確認/1_ハイチーズ.png
に保存されるようになりました。
テスト環境によってはスクリーンショットは必要ない
ENV
でtake_ss
の動作を変更すれば、テストケースの内容に手を触れることなく対処できるので、安心だと思いました。
おわりおよびその他
Reactがちゃんと動いた
以前はJavaScriptの、特にReactのテストにはJSDOMを用いたりもしましたが、やはりブラウザ経由のクリッククリックを自動で行えるのは便利だと思いました。
Capybara
単独でテストもできる
Capybara
はCapybara.app_host
を設定すると好きなホストにアクセスできます。スクリーンショット好きな方は汎用テスト環境としていかがでしょうか。
前職では、途中からかかわることになったテスト皆無のプロジェクトのテストを外部から行ったりしていました。
また、最近ではCSRFを発見したサイト管理者への連絡や、再現(および修正の確認)のためにも使用しました。
撮りまくった図です。
:link: 自殺を知る、自殺を考える :: 自殺者数チャート
正直、自分でつくったページのスクリーンショットを眺めるのが好きなだけかもしれない気もしました。
画像を連結する
montage
というコマンドが使えました。
乱暴なrbファイルを用意して連結しています。
files = Dir['./log/ss/**/*.png']
file_names = files.join(' ')
w = 20
h = (files.size / w.to_f).ceil
`montage -background black -tile #{w}x#{h} -geometry 40x40 #{file_names} montaged.png`