Capybaraでのテスト中にスクリーンショットを撮りまくって幸せになる。

Capybaraでのテスト中にスクリーンショットを撮りまくって幸せになる。

業務でのスクリーンショットといえば、エクセルとあわせて語られるようなつらい話がセットになっている印象があります。

マニュアル撮影するのは本当につらそう……でも自動テストで自動で撮るスクリーンショットは最高です。 わたしは大好きです。

そのスクリーンショットを、テストついでのなんとなくみたいなノリで簡単に撮れるようにしておくといろいろ便利でしたので、どのようにやっているか書きます。

CapybaraPoltergeistを使用、PhantomJSのバージョンは2.0の環境下で書きました。

ところで、スクリーンショットが役に立った局面

前職では、責任をとるための上司と、名ばかりディレクター、そしてわたしという編成で仕事をすることがわりとありました。

必然的にアプリケーションの細かいところも全部わたしが直接やりとりしていましたが、基本的に先方も別のお仕事をなさっているので、はっきり言って開発中だとクリックさえもしてくれないわけです(まぁ納品直前もあまり積極的にクリックしてくれない、例外はあまりない)。

そんな時にスクリーンショットが大量にあると、画面はこういう流れです、文言はこうです、配置はこう、間違ってないか即座に返答してください早くしてくださいという話がとても早くなりました。

その前にCapybaraでテストしやすく作っておく

Capybaraで行うテストは、一般的に以下のような流れになると思います。

  1. querySelectorなシンタックスなどを用いて要素を特定
  2. clickしたりfillしたりする
  3. 再び要素を特定しての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はそのテストに関するメタ情報などが入るクラスです。

itscenarioのブロックに引数としてインスタンスが渡されますので、これを参照し、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に保存されるようになりました。

テスト環境によってはスクリーンショットは必要ない

ENVtake_ssの動作を変更すれば、テストケースの内容に手を触れることなく対処できるので、安心だと思いました。

おわりおよびその他

Reactがちゃんと動いた

以前はJavaScriptの、特にReactのテストにはJSDOMを用いたりもしましたが、やはりブラウザ経由のクリッククリックを自動で行えるのは便利だと思いました。

Capybara単独でテストもできる

CapybaraCapybara.app_hostを設定すると好きなホストにアクセスできます。スクリーンショット好きな方は汎用テスト環境としていかがでしょうか。

前職では、途中からかかわることになったテスト皆無のプロジェクトのテストを外部から行ったりしていました。

また、最近ではCSRFを発見したサイト管理者への連絡や、再現(および修正の確認)のためにも使用しました。

撮りまくった図です。

montaged.png

: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`