upinetree's memo

Web系技術の話題や日常について。

Minitest の Unit style で DSL 風な around ヘルパーを定義する

RailsActiveSupport::TestCase を使っているとして、サクッとミニマムにやりたい。

minitest-around を使うと、以下のような around メソッドを定義することで setup/teardown をセットで実行できる。

require "test_helper"
require 'minitest/around/unit'

class CatTest < ActiveSupport::TestCase
  def around(&block)
    p "konnichi nyan"
    block.call
    p "sayouna nyan"
  end

  def test_nyan
    # ...
  end
end

でも、なんとなく ActiveSupport::TestCase が用意している setup, teardown と揃えてDSL風に使いたい。

require "test_helper"
require 'minitest/around/unit'

class CatTest < ActiveSupport::TestCase
  # こうしたい!
  around do |test|
    p "konnichi nyan"
    test.call
    p "sayouna nyan"
  end

  # これと同じ感じ
  setup do
    @cat = Cat.new
  end
end

なので test_helper.rb でこうする。

class ActiveSupport::TestCase
  def self.around(&block)
    define_method(:around) do |&test|
      instance_exec(test, &block)
    end
  end
end

Unit style でどこまで複雑なテスト設計に対応できるかは置いておいて、ちょっとした便利ネタでした。

Docker + Rails開発環境Tips

普段なんとなくで設定していたのですが、今の所こんな感じでやるのがいいんじゃない?というのをそろそろまとめたくなったので書いてみました。 もっといい方法があれば教えてください〜。

bundle install の実行のたびにDockerイメージのビルドし直しになるのを回避する

Quickstart にあるような構成では、以下のように Dockerfile で bundle install する。

FROM ruby:2.5.3

RUN mkdir /app
WORKDIR /app

RUN gem install bundler
RUN bundle install

この構成はgem一式を含めてポータブルにするという意味では合理的なのだが、開発環境においてはそうでもない。なぜなら、開発環境ではGemfileの更新やgemのアップデート、Dockerfileの修正が頻繁に発生し、そのたびに各開発者のローカルマシンでのイメージのビルドが必要になってしまうからだ。

これを回避するには、イメージの外、つまりコンテナで bundle install を行って gem を保存すればよい。ただし、コンテナを破棄した際に gem も消えてしまうと困るので、キャッシュできる必要がある。そのためには以下の方法がある。

  • bundle install 用のディレクトリをホストと共有し、そこに格納する
  • gem 用の volume を用意し、そこに格納する

前者のほうがお手軽だが、後者のほうがディスクアクセス的に有利そうではある(未計測)。もしホストと共有したくないとか、パフォーマンス上問題になってきたら後者に乗り換える形でどうだろうか。

bundle install をコンテナで行い、そのディレクトリをホストと共有する

BUNDLE_PATH でインストール先をホストと共有するディレクトリ(この場合は /app 配下の /app/vendor/bundle)に指定。

FROM ruby:2.5.3

ENV APP_PATH=/app
ENV BUNDLE_PATH=$APP_PATH/vendor/bundle

RUN bundle config path $BUNDLE_PATH

なお、 Bundler は BUNDLE_PATH , .bundle/config の順で優先してパスの設定を読み込む。そのためどちらかを指定していれば問題ないが、まれに .bundle/config の設定がないとRailsrequire 'bundle/setup したときに BUNDLE_PATH を見てくれないことがあった。そのため上記の例では両方設定している(原因特定できておらず、環境要因かもしれないので要確認)。どの設定が効いているかは docker-compose run --rm app bundle config で確認できる。

docker-compose.yml は以下のように /app 配下をホストにマウントする。 vendor/bundledelegated フラグ付きでマウントすると高速に同期できる(ただし、コンテナ→ホストへのWriteパフォーマンスが犠牲になる。bundler配下ではほぼ発生しないので問題なし)

version: '3'

services:
  app:
    build: .
    command: ['./bin/rails', 's']
    tty: true
    stdin_open: true
    ports:
      - '3000:3000'
    volumes:
      # cached/delegated options for performance tuning
      - .:/app:cached
      - ./vendor/bundle:/app/vendor/bundle:delegated
      - ./node_modules:/app/node_modules:delegated

gem 用の volume を用意し、そこに格納する

BUNDLE_PATH はマウントされないところにしておく。

ENV APP_PATH=/app
ENV BUNDLE_PATH=/bundle
RUN mkdir $APP_PATH
WORKDIR $APP_PATH

そして docker-compose.yml でボリュームを用意し紐付ける。

version: '3'

services:
  app:
    volumes:
      - bundle:/bundle
volumes:
  bundle:
    # use default driver

binstub も考慮したい場合は、以下のようにすると良いらしい。これがなくても手動で binstub を生成する(そしてリポジトリに含める)ことは可能な気がするが、この方法の便利なところは entry point を定義しておくと docker-compose up のたびに最新の状態に保てることだろう。そういう意味では、 bin/setup を entry point に登録しておくと便利なのかもしれない。

https://medium.com/@jfroom/docker-compose-3-bundler-caching-in-dev-9ca1e49ac441

デバッガによる対話コンソールを有効にする

よくある docker-compose.yml では、以下のように docker-compose upRailsサーバも一緒に起動するようになっている。

services:
  app:
    build: .
    command: ['./bin/rails', 's', "-b", "0.0.0.0"]

この設定だと標準入力が閉じられているので、 binding.prybinding.irb のような対話コンソールによるデバッグが行えない。従って以下の設定を追加する。

services:
  app:
    build: .
    command: ['./bin/rails', 's', "-b", "0.0.0.0"]
    tty: true
    stdin_open: true

ただし、 docker-compose up でコンテナを起動した状態では、デバッガが起動した内容のログを確認できても、対話コンソールにアクセスできない。Railsサーバのコンテナは docker-compose up のプロセスの後ろにいるためのようだ。従って一工夫する必要がある。2通りの方法を紹介する。

Railsサーバのコンテナに attach する

docker attach コマンドを使うと、起動中のコンテナの標準入出力に接続することができる。これをRailsサーバのコンテナに利用する。具体的には次の操作を行う。

  1. デバッガのブレークポイントを仕込んで、そこを通る処理を実行する(処理を通って対話コンソールが待機状態になっているかどうかは docker-compose up のログ、もしくは docker-compose logs でも確認できる)
  2. 別のターミナルで docker attach $(docker-compose ps -q app) を行って接続。接続直後は何も出力されないので、エンターキーを押して対話コンソールが起動していることを確認すると良い
  3. デバッグを行う
  4. 対話コンソールを終了する(ctrl-dquit などで)
  5. コンテナへの接続を解除する。 ctrl-p ctrl-q を連続して入力(デフォルトの設定。 docker attach --detach-keys="<sequence>" で変更可能)。うっかり ctrl-c を押してしまうとコンテナごと終了してしまうので注意

このように、Dockerを利用せずにローカルでサーバを起動する場合とは全く感覚が異なる。特にチームで作業する場合などでは、効率を上げるためにも bin/attach のようなスクリプトを作っておくと良さそうだ。

#!/usr/bin/env bash
echo attach to $1 ...
docker attach --detach-keys="ctrl-c" $(docker-compose ps -q $1)

次のような起動スクリプトでさらにラップしても良い。

docker-compose up -d
bin/attach app

Railsサーバのコンテナだけ docker-compose up とは別に起動する

attach を利用する方法で以下のような懸念がある場合には、Railsサーバを独立したコンテナで起動すると便利だ。

  • bin/attach のようなスクリプトを作っても、意識のアップデートが必要。Dockerに慣れていない場合にハードルがある
  • Railsサーバのコンテナに変更や試行錯誤が多く、再起動する機会が多い
    • up -d で起動して stop appstart app で個別に再起動することはできるが、フォアグラウンドで起動、終了を制御したい

このためには、まず docker-compose.yml で次のようにRailsサーバを起動しないようにする。ここではかわりに bin/update を実行して環境の最新化を行うようにしている。

services:
  app:
    build: .
    command: ["./bin/update"]

そして、Railsサーバは次のようなスクリプトで起動する(Makefileに入れるのが楽だ)。

docker-compose up -d
./bin/sleep_until_app_ready
docker-compose run --rm --service-ports app bin/rails s

注意点は —service-ports オプション。 docker-compose.yml で設定した ports が、 run のときにはこのオプションがないと反映されない。

https://docs.docker.com/compose/reference/run/

The second difference is that the docker-compose run command does not create any of the ports specified in the service configuration. This prevents port collisions with already-open ports. If you do want the service’s ports to be created and mapped to the host, specify the --service-ports flag

途中で使っている ./bin/sleep_until_app_ready は以下のような簡単なスクリプトで、 bin/update が終了するのを待っている。

#!/usr/bin/env bash

while :; do
  sleep 1

  docker-compose exec db echo 'alive?' &>/dev/null
  DB_UP=$?

  docker-compose exec app echo 'alive?' &>/dev/null
  APP_UP=$?

  if [ $DB_UP -eq 0 ] && [ $APP_UP -eq 1 ]; then
    break
  else
    echo -n "."
  fi
done

echo "Ready for running app"

対話コンソールで利用されるページャ

ruby イメージはデフォルトでは less を備えていないので、デバッガは ruby 実装の自前ページャを利用する。ローカルで動かしたときのように less で見たい場合は、インストールするように Dockerfile を修正すると良い。

Springサーバーを有効にする

spring-docker-example が参考になる。

基本的には以下のようにSpringサーバ用のコンテナを立てて、

services:
  app:
    build: .
    volumes:
      - .:/app:cached
    depends_on:
      - db
      - spring

  spring:
    build: .
    volumes:
      - .:/app:cached
    command: ["./bin/spring", "server"]

spring.sock をコンテナ間で共有するだけ。そのために、 sock ファイルを tmp ディレクトリ配下(ホストにマウントして共有)に置くように、 config/spring_client.rb (Springが起動時に読み込む)でその場所を教えることにする。環境変数なので、もちろん他の方法で指定しても良い。

ENV["SPRING_SOCKET"] = "tmp/spring.sock"

コンテナをまたいだ spring の自動立ち上げはできない(方法あったら教えてほしい)ので、適宜 depends_on: spring を設定しておくと良い。

注意したい点として、 Springサーバのコンテナの環境変数を、Railsサーバのコンテナの環境変数と揃えておくこと。Springサーバのコンテナが異なる環境変数を利用し、それを元に各種定数を初期化してキャッシュすると、Railsサーバは異なる環境変数を元にした定数を参照することになる。例えば database.yml にERBで環境変数を埋め込んでいる場合に、意図通りに設定されない可能性がある。

default: &default
  host: <%= ENV.fetch("DATABASE_HOST") { 'localhost' } %>

同様の理由で、マウントするディレクトリやボリュームも同じ条件にしておくのが安全。Spring経由での Dir.glob の結果が異なることがあるため。

なお、 spring-docker-example の例では、ホストとコンテナの両方で同じ spring.sock を共有し、さらに SPRING_SERVER_COMMAND='docker-compose up spring' を設定したりPIDのネームスペースをホストにしたりして、ホストからSpringを利用して bin/rails c を行うことができるようにしている(というか、そもそもホストにRubyとSpringを入れて起動するのを前提にしているようにみえる)。ホストとの共有はまだ試したことがなくて、きっと spring コンテナの環境での Rails コンソールが立ち上がると思うのだが、本当に macOS 上の環境に依存しない( native extension の違いとか、 webpacker が実行する yarn check --integrity の結果とかの影響を受けない)のか気になるところ。今のところはホストからの利用はせずにコンテナ上で完結するように設定しているが、そのうち試したい。

「現場で使える Ruby on Rails 5速習実践ガイド」の執筆に参加して現場の知見を書きました

この度、2018/10/19にマイナビ出版から発売される書籍「現場で使える Ruby on Rails 5速習実践ガイド」の執筆に参加しました。

書籍情報

今ご購入いただくと、購入特典として未収録PDFがもらえます!

https://book.mynavi.jp/pcbook/blog/detail/id=97187

特典は紙・電子本両方に付きます。配布は2018年10月24日(水)ごろになる予定です。

どんな本なの?

Ruby on Rails の基本的な使い方の解説から、複数人での開発やメンテナンスしやすい状態の維持などの実践的なトピックにまで広くカバーしています。

想定読者の中心を「他言語でのWebアプリケーション開発経験のある方」として設定していますが、初心者の方でも理解しやすいよう段階的に学べる構成に仕上げました。その結果480ページと盛り沢山なボリュームとなっており、索引から知りたい箇所を調べるといった辞書的な用途にも十分使えるかと思います。

本書の特徴

タイトルにもある通り、現場で得た知識や経験をところどころに散りばめています。例えば、チームでアプリケーションを開発するときにどうコードを共有してコミュニケーションを取るのか、長く生きることになりそうなアプリケーションの保守性をどう持続して行くのかという話題について、多くのページを割いて丁寧に説明しています。Railsを学習する最初のステップから発展して、実際に仕事で使うような次のステップでも本書が役に立つと思います。

読者が本書を超えて次のステップに進めるように、というのは特に重視した点です。そのための仕掛けとして、本書で解説はしないけれども大事だという概念はキーワードだけ出してみたり、脚注で補足したりと、興味があれば自力で調べて学習していけるようなきっかけを作っています。またRailsGuidesやAPIリファレンスなどを都度参照するようにして、Railsコミュニティの信頼できる情報ソースを見る習慣を付けられるようにもしています。

Rails 5.2 かつ Ruby 2.5.1 と、今出ているバージョンの最新に追従している点もアピールしたいところです。ActiveStrage や Credentials, Content Security Policy, Webpacker といった最近のRailsの機能にも触れています。「数年間Railsからちょっと離れちゃったけど、最近のRailsについて包括的に勉強し直したい」といった用途にも有用です。

逆に本書で扱っていない話題は、Railsアプリケーションの運用です。本当は触れたかったところではあるのですが、あくまでRailsの機能やRailsアプリケーションの作り方にフォーカスする形で限られた紙面を使いました(それでもなかなかのボリュームになってしまったのですが)。運用のために利用できるRailsの機能は紹介していますが、どこにどうやってデプロイするかといった話題は他の良書で学んでもらえたらと思います。

いくつかサンプルを貼るので参考にしてください。2色刷りで図もコードも見やすくなっています!

f:id:upinetree:20181016181506j:plainf:id:upinetree:20181016181348j:plainf:id:upinetree:20181016181341j:plainf:id:upinetree:20181016181349j:plain

個人的な担当範囲と感想

2章、6章、8章、9章、10章を中心に関わりました。特に10章は本書の特色を仕上げる章にしたくて中心的に力を入れました。

執筆する上で意識したのは、Railsらしさとはなんだろうというところから、Railsらしく書くことのメリット、それを外れるときの判断基準やリスク、その対処方法、Railsの外の設計パターンとRailsの取った道…などなど、いろいろ考えました。なんとなく理解していてもここまで突き詰めて考えたことはなかったので、執筆する中で自分としてもとても勉強になりました。Railsのコードも結構読んだので、この処理はあの辺ね〜という脳内地図ができたのも収穫のひとつです。

あとは、なるべくコンテキスト依存の単語を使わないようにしようとか、平易な言葉で説明しようということを意識しました。オブジェクト指向ソフトウェア工学をやった人には「関心事」「結合度」「オブジェクトの責務」などの表現がすごく便利で話が早いのですが、今回はRailsの学習に集中するために極力使わないようにしてみました。意識してみて、自分がいかに普段特定のコンテキストに生きているのかを感じました…。

苦労した点

たくさん苦労しました。

Railsが(というかウェブアプリケーションが)対象とする範囲が広く、この概念を説明するにはこれがいる、というのがとても多いので、「あー、あれの説明がないからこれを紹介できない!」となることが多かったです。互いに関連しあっている話題もあり、そのうえ複数人で執筆していることから調整が難しいという点も要因の1つでした。

そして、この話題はこの章に移動したほうが良いなど、読者の理解の順や話題の類似性などを考慮して組み替えることも多々ありました。しかしただ組み替えただけでは、それぞれ独立した話題が寄せ集まっただけで流れが悪くなってしまいます。そのためそこにストーリーを付ける必要があるのですが、それがなかなかに苦労しました。特に10章は難しかったのですが、(共著者の)大場さんの力をかなり借りてとても良い形にまとまったと思います。

また、説明したいことはいろいろと思い浮かんでも、それを端的に分かりやすく伝えるためのサンプルコードを考えるのに苦労しました。悩んだ末「これだ!」と思いついた例が、帯に短し襷に長しであったり、抽象的すぎたり具体的すぎたりファンタジーすぎたりと、ちょうどよいのが思い浮かばない。これはある種のトレードオフスライダーみたいなもので、結局ある程度の落とし所を見つけるのですが、ちゃんと伝わりやすくなっているかなと今でも心配です。これが一番大変だったかもしれません。

おわりに

こういった苦難を乗り越えてようやく出版されることになり、とっても清々しい気持ちです。まだまだブラッシュアップできるんじゃないとか、やりきれていないんじゃないかといった自問もありますが、そうやってると一生終わらないので、今はとにかく終わってよかったという気持ちです。といいつつ、手元に届いた見本誌を見ると、良い本に仕上がったんじゃないかなと思っています!

以上のように、現場の知識や経験をこねくり回して濃縮したのが「現場で使える Ruby on Rails 5速習実践ガイド」です。ぜひ手にとってみてください。

合わせて読みたい

t2os.hatenablog.com

ppworks.hatenablog.jp (早速のレビューありがとうございます!)

BackstopJS を利用した Visual Regression Testing の感想と工夫ポイント

Visual Regression Testing とは

CSSやコードの変更によって、デザインに意図せぬ影響・リグレッションがないかを視覚的に確認するためのテスト。 変更前後のスクリーンショットを撮影して、差分を出力する方法が一般的。

似たテストに Snapshot Testing があるが、これはHTMLやASTを比較するテストであるため、CSSを含めたデザインの確認はできない。

やってみた感想

CSSリファクタリングでは、見た目が変わっていないことや、見た目の変更が意図通りであることを確認する工程が多くを占める。 変更が大量にある場合はすべてをチェックすることは現実的ではないため、ある程度サンプリングして確認することになる。従って確認漏れの可能性は残る。 さらに、目視での確認は不確実でもあるし、何度も行っていると疲れて精度が落ちてくる(あと、とにかくつらい… :sob: )。

テストがあると、これらの作業を効率化したうえで、安心してリファクタリングできるようになる。 具体的には、次のシチュエーションで効果を実感できた。

  • 全体のレイアウトに広く影響があるような修正を行うとき
    • たとえば、プロジェクト初期のCSSの負債を返すとか、以下のようなケースでは思いもよらぬ影響が起こりやすい
      • クラス名から推測できないような使われ方をしてるクラスの責務を適切に分離するとき
      • 無秩序にカスケードしていたクラスを整理するとき
      • 広くマッチするセレクタの範囲を狭めるとき
    • そんな時でもある程度安心して変更を加えられる
  • 新しく実装する機能のマークアップを行うとき
    • 最初は雑にマークアップして、ひとまず最短で体裁を整えることを目指す
    • その後、見た目を変えないよう設計をリファクタリング、というのがストレスなくできる
  • 副次的に、レビュー依頼など変更を説明するとき
    • レポート画面をスクショして提示するだけで、before/after+差分画像で変更を説明できて楽かつわかりやすい

プログラミングの自動テストと同じ感覚で、効率化と安心感をもたらしてくれた。

逆に微妙な点は、

  • 実行時間が長い
    • アプリケーションや環境にもよるが、100画面超えてくると10分超かかる
    • 並列実行数を増やすとテスト結果が不安定になるので高速化にも限界がある
    • 流して放置しつつ休憩 or 作業することができるので、目視時代よりは格段に楽
  • テスト結果が不安定
    • 今のところ開発環境で立ち上げたサーバでテストしているので、データの状態や実行時間などの影響が大きい
    • スクリーンショットを撮影するタイミングで結果が変わることがある
    • 上記は工夫次第で解決できそうだが、そのための労力が必要

BackstopJS

今回は、BackstopJSを使って実施した。 BackstopJSはスクリーンショット撮影、比較、比較結果のビジュアライズ、レポートUIといった、テストに必要な機能を十分満たしていて、かつメンテナンスも活発であることから採用した。 最近の開発シーンだとStorybookなどと組み合わせて行うのがモダンなのだろうけど、今回はそこまでJSを多用するプロジェクトではないのと、コンポーネントを組み合わせたときのレイアウトも確認したいため、どんな画面でもテストできることを重視した。

BackstopJSでは、例えばテストに失敗すると、以下のように差分が確認できる。

f:id:upinetree:20180513230107p:plain

before/after の目視での比較も可能。

f:id:upinetree:20180513230126g:plain

詳細はBackstopJSのREADMEを確認されたし。

利用にあたっては、標準の状態では使い勝手の良くない部分があったので色々工夫した。 BackstopJSはブラウザの操作に使うスクリプト./backstop_data 下に出力するので、そこをカスタマイズすることで大体解決できる。

そのカスタマイズ結果を他のプロジェクトでも使えるように汎用化して、雛形として公開してみた。

https://github.com/upinetree/backstop_boilerplate

大体コードを眺めればわかるのだが、せっかくなので以下では工夫したところやハマったところを解説してみる。

工夫ポイント

複数クリックイベント

オプション clickSelector はクリックするセレクタを1つしか指定できないが、あるセレクタをクリックして、次にこのセレクタをクリック、ということをしたい場面は結構ある。 そのため複数のセレクタを連続でクリックする clickSelectors を指定可能にした。

// backstop_data/engine_scripts/chromy/clickAndHoverHelper.js
const clickSelectors = (scenario.clickSelectors || [scenario.clickSelector]).filter(x => x);

clickSelectors.forEach((clickSelector) => {
  console.log(`INFO: click ${clickSelector}`);

  chromy
    .wait(clickSelector)
    .click(clickSelector);
});

(Chromyを利用しているが、現時点でBackstopJSはPuppeteerにも対応しているので、そっちでも利用できるようにしたい…)

UserAgent を設定する

viewport の設定による画面サイズごとのテストはサポートしているが、標準ではそこにUAを付けてくれない。 なので、 viewport の内容を見てUAを切り替える処理を自分で書き加える。

backstop.jsononBeforeScript に指定されているスクリプトで、以下を記述。

// backstop_data/engine_scripts/chromy/onBefore.js
if (vp.label === "phone") {
  chromy.userAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1");
} else {
  chromy.userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36");
}

Cookieの読み込み動作周り

初期状態では、Cookieは毎回 cookiePath オプションでテストシナリオごとに指定しなければいけないが、ログインが前提のアプリケーションでは使いづらい。 デフォルトのCookieを読み込むように変更した。

// backstop_data/engine_scripts/chromy/loadCookies.js
const defaultCookiePath = "backstop_data/engine_scripts/cookies.json";
const cookiePath =
  scenario.cookiePath === "" ?
    "" :
    scenario.cookiePath || defaultCookiePath;

また、BackstopJSが生成したクッキー読み込みスクリプト loadCookies.js ではテストシナリオごとにCookieの中身を出力するので、ログが流れてしまい追いづらい。 必要ないときは出力を抑制するようにした。

// backstop_data/engine_scripts/chromy/loadCookies.js
if (process.env.VERBOSE) {
  console.log("Cookie state restored with:", JSON.stringify(cookies, null, 2));
} else {
  console.log("Cookie state restored.");
}

テストシナリオの複数JSONへの分割機能

BackstopJSは標準ではテストシナリオを backstop.json に以下のように他の設定と同列に書く。

scenarios: [
  {
    "label": "xxx",
    "url": "http://localhost:3000/xxx",
    "selectors": [
      ".aListOfStuff li"
    ]
  }
]

これだと、テストシナリオが増えたときに管理しづらくなる。

そのため backstop_data/scenarios 内の複数のJSONファイルに分割して管理できるようにした。 実行時にはこれらのJSONをそれぞれ読み込み、以下のようにひとまとめにする( backstop_data/scenarios/index.json )。

const fs = require("fs");

const jsonFiles = fs.readdirSync(__dirname).filter(filename => filename.match(/\.json$/));
const scenarios = jsonFiles.reduce((acc, filename) => (
  acc.concat(require(`./${filename}`))
), []);

module.exports = scenarios;

こうしてひとまとめになったJSONをBackstopJSに渡す、という処理を backstop_data/runner.js に記述し、yarnスクリプトから呼び出すようにした。 (runner.js については後述)

backstop_data/runner.js

const backstop = require("backstopjs");
const config = require("../backstop.json");
const scenarios = require("./scenarios");

config.scenarios = scenarios;

backstop(command, {
  filter,
  config
});

package.json

{
  "scripts": {
    "backstop": "node ./backstop_data/runner.js"
  }
}

テストシナリオのJSONへの変数展開機能

例えば商品詳細画面のように、テスト対象のURLには動的なものもある。 そのようなURLを決め打ちでJSONに記述すると、以下のようにテスト環境が変化したときにURLが無効になってしまう。

  • URLのパラメータにしていたレコードの ID, UUID, 名前, etc が変わる
  • レコードが削除される

このような環境変化をある程度吸収するために、JSON読み込み時に変数展開を行えるようにした。 たとえば次のように、 ${} で埋め込んだ変数を展開してテスト対象URLを動的にできる。

{ "url": "http://localhost:3000/posts/${post_id}" }

post_id が 1 なら…
=>  { "url": "http://localhost:3000/posts/1" }

具体的な実装はシンプルにこんな感じ( backstop_data/interpolate.js )。

const variables = require("./variables");
const keys = Object.keys(variables);

module.exports = (json) => {
  const jsonStr = JSON.stringify(json);

  const interpolated = keys.reduce((acc, key) => (
    acc.split("${" + key +"}").join(variables[key])
  ), jsonStr);

  return JSON.parse(interpolated);
};

これを runner.js が呼び出して、BackstopJSに渡す前に適用している。

変数を展開するための値は、 backstop_data/variables.json に定義するようにした。

{
  "post_id": 1,
  ...
}

また、このJSONを手動で変更するのはつらいので自動で生成するスクリプトを書いた。 今回はRailsプロジェクトなので、次のようなRakeタスク。

desc 'Dump a JSON file for the backstop variable interpolation'
task dump_backstop_variables: :environment do
  post = Post.first!

  json = JSON.pretty_generate({
    post_id: post.id.to_s
  })

  File.open('backstop_data/variables.json', 'w') { |f| f.write(json) }
end

透過的なyarnスクリプトを用意する

次のように、yarnスクリプトを利用する場合でも、BackstopJSのサブコマンドやオプションを透過的に利用できるようにしたかった。

$ yarn run backstop test
$ yarn run backstop test --filter="<scenario label>"
$ yarn run backstop reference
$ yarn run backstop approve

JSONを結合したり、JSONに変数を展開したりといった機能を実現するために runner.js を作成したので、yarnスクリプトから引数を受け取って、うまいことBackstopJSの関数に渡す必要がある。 パーサをrequireするのも良いのだが、シンプルな用途なのに依存を増やすのもあれだったので、自分で軽く書いた。

backstop_data/runner.jsを参照。

ハマリポイント

スクリーンショットの撮影に失敗しやすい

テストレポートにスクリーンショットが出ない場合、撮影に失敗している。 サーバーが起動していて、かつ selector などの指定が明らかに正しい場合は、 backstop.jsonasyncCaptureLimit の値を下げてみると改善する。

この値はヘッドレスブラウザの並列起動数で、メモリが足りなかったりすると撮影に失敗することがあるようだ(詳しくは調べていない)。

他にも、サーバー立ち上げ直後だとアセットの更新(webpackだったり、Railsならアセットパイプラインだったり)で時間がかかりタイムアウトする場合がある。 事前にページを開いておく、webpackbin/rails assets:precompile しておくなどでテストを安定化できる。

デザイン崩れに起因しない差分が出やすい

テストの性質上、冪等性が保ちづらい。 デザインは崩れていなくても、スクリーンショットに差分が出ることが多くある。 例えば、

  • レコードの増減、変更
  • 時間経過による動的な表示
  • ランダム表示

などの影響を受けやすい。

あまりに動的で毎回差分が出るような要素はあらかじめ hideSelectors で非表示にすると良い。 もしくは特定要素のみの確認で良ければ selector でその他の要素の影響を避けることもできる。

差分が出て、デザイン崩れに起因しなければ、その都度 backstop approve する。

Ruby25の聴講メモ

昨日(2/24)、Ruby25に参加してきました。 最初は小さくお祝いするのかなと思ってたら、予想以上に大きなカンファレンスでびっくりしました。参加できてよかった。 いろいろあって最近仕事でRubyをあまりキメられていないのですが、やっぱ良い言語で良いコミュニティだなと再確認しました。 また、久々に会えた方々とお話できて良かったです。

以下、聞いた講演のメモです(まとめられてないです)。 聞き間違えや解釈の違い等あるかもしれません。

高橋さん「Rubyの1/4世紀」

  • Ruby」の頭文字が大文字なのは、連載「ちょーわかりやすい Perl & Ruby 入門」で「ruby」のように小文字だと「Perl」との並びが悪いので大文字になったらしい
  • かつて主要なパッケージ管理にはRPA, RubyGemsがあり互換性なかった。RailsがRubyGmesを採用したことで普及
  • rubygems.org は Gemcutter が前身
  • パッケージ管理やbundlerなど、Rubyのエコシステムの構築をRailsがリード

Matz「Ruby after 25 years」

  • ソフトウェアの誕生には物理的実体がない。概念的
    • Rubyという概念とは、名前?
    • Rubyという名前が生まれたときが、Rubyという概念の誕生
  • 未来の話してと言われたけどむずかしい。当たるも八卦当たらぬも八卦

この25年をふりかえる

  • プラットフォームとしてのOSはほぼOS。WindowsすらWSL
  • CPUもあんまりアーキテクチャが変わらない
  • プラットフォームは多様性が減少する一方
  • コンピュータの普及、性能の向上、モバイルの台頭
  • サーバサイドでなんでもやる時代ではなくなった。モバイルアプリ、SPA
  • トレンドの変化がある
  • 変化の傾向として、ものすごくスケーラブルに、分散処理

これを踏まえて未来のRubyは?

  • Rubyのコアは変わらないのでは
    • チューリング完全性という最低限は備えているので劇的な変化は必要ない
    • 文法が大きく変わったらRubyとはいえない
  • プログラミング言語のテーマは生産性の向上
    • より早くより簡潔に書ける、頭の中を直接コンピュータに伝えられる
    • 優れた抽象モデルが提供できる
    • 保守性がある
  • 現在のRubyは小さなチームではじめられる

近未来のRuby(Ruby3)

  • 高速、分散、解析
  • Ruby 3x3
  • やればできるゴールではなく、できるかもしれないゴール、わくわくさせるようなことを提示するのが私の役割
  • 言ってみるもんで、案外できそうなところまできた
  • 2020年目標(約束はしない)
  • 過去に学んで、断続的な変化はしない。Ruby3は単なるラベルにする

その先のRuby

  • インタラクティブプログラミング
    • 入力したら警告がポップアップで出てくるみたいな感じ
    • 静的解析技術、実行時プロファイリングなどの発展系
    • 賢いテディベア(テディベアプログラミング)がコンパイラについてくるなど
  • 分散技術の発展とともに、分散処理の抽象化、スケーラブルなアーキテクチャが必要になってきている
    • Webもいい線行っているが万能ではない
    • FaaSが現在では近いと予想
    • Rubyでは、Guildのその先ができるといい
  • 非均質的計算環境対応
    • BigLITTLE、GPGPU
    • FPGAもある種の非均質的環境
    • どう抽象化し、同じシステムとして扱えるようにする?

むすび

  • 人類は25年ではあまり変わらないが、文化は変化する
    • 「コンピュータは楽しい」があたりまえになってほしい
    • 思考ツールとしてのRuby、人間のためのRubyにしていきたい
  • 最大の目標は「プログラミング言語サバイバル」
    • 生き残るために価値、楽しいプログラミングを提供し続ける

松田さん「Ruby on Ruby on Rails

  • RailsRubyの良さを伝えるためのインフラ
  • 先週くらいから6.0を開発中
  • 5.2はコミッタの中では終わったRails
  • これからもRailsは変わり続け世の中を変え続けるので必死こいてバージョンアップして下さい

近藤さん「 RubyとInfrastructure as Code、そして大規模インフラ」

  • Rubyはどう Infrastructure as Code と関わっていくか
  • RubyDSLを作るのに向いている
    • なのでDSLを採用したプロダクトが多い(Cheff, Itamae, Serverspec...)
    • 特にConfigurationのツールが多い
  • 直近のインフラコードの潮流
    • マイクロサービス、コンテナ化、サーバレス
    • ウェブインフラの大規模化、アプリの複雑さが背景
  • 支援団体 Cloud Native Computing Foundation(代表プロジェクト: Kubernetes)
    • この団体の支援プロジェクトの中で、Ruby製は1つだけ(最多はGo)
    • まだクラウドインフラ分野に進出の余地がある
    • mrubyで実現する未来を作っていきたい

石井さん「 mruby、今IoT、組込界隈でこう使われています、最新事例紹介!」

  • 組み込み技術者不足
  • webの人にも組込してもらえたら良いよね、ということでmruby
    • ruby, mruby, mruby/c で世の中のなんでも作れる
    • 試作しやすさ、文字列処理のしやすさ
    • ただし弱点として何msec以内に〜をしなければ、というのを保証できない
    • それ以外のエンタメとかいろいろな面で利用価値がある
  • しかし、なかなか使ってもらえなかった
    • 組み込みは一回製品を出荷したら変更できないのですごく慎重
  • 軽量Ruby 普及・実用化促進ネットワークで技術情報を提供中
  • これからのmruby
    • Ruby2.0をキャッチアップ(現在1.9ベース)
    • グローバル会議やりたい(来年)

田籠さん「Data Processing and Ruby in the World」

データ処理のステップは collect => summarize => analyze => visualize

この世界でのRuby

  • collect
    • fluentd
    • logstash (JRuby)
    • embulk
  • summarize, analyze
  • Rubyを使ったサービス
    • TD
    • Repro
    • FlyData

という感じになっている

これからの機械学習系プロジェクト

他の言語ではすでに実装されているものもあるが、全体のエコシステムで不自由なく使えることがとても大事。

Matz & Miyagawa 未来を語る特別対談

  • MJITは最初はオプトインで提供
    • 今後の性能特性を見てデフォルトにするか判断する
    • (2.6-preview1 がさっきでた)
  • steepによる型推論、別ファイルとしての型情報
    • TypeScriptの型定義ファイルみたいな感じ
    • 型を指定したときの安全感は別の気持ちよさがあるのでは
  • 言語のシンタックスは互換性を保つことを考えると厳しい。もう限界が来ている
  • Ruby3には間に合わないが、パッケージ、ネームスペースの再考したい
    • JSみたいにrequireの返り値がモジュールとか(現在Rubyはtrue)
    • ちなみにrequireは複数の引数を受け取れるが、全部のrqeuireをトランザクションで保証するわけではない

RailsのRoutingの名前付けでハマった

Astrum などのスタイルガイドジェネレータを利用する際、Railsの外からコンパイル済みCSSを読み込みたいことがある。

このとき、アセットパイプラインを利用している場合はdigestを含めて参照する必要があるのだが、Railsの外の世界で、かつ変更に開いていない場合いい感じにdigestを埋め込むことができない。

そこで今回はRailsのコントローラ経由でCSSをリダイレクトしてしまうアイデアで実現したのだが、Routingのリソース名でハマった。

class AssetsController < ApplicationController
  def show
    redirect_to helpers.stylesheet_url('hogehoge')
  end
end
resource :asset, only: :show

こうするとなにがまずいかというと、 asset_url が上書きされてしまうのだ。

従って一部のアセットが正しく取得できなくなる。 SassのURLヘルパーもこのメソッドを利用しているので 1CSSに埋め込んだimageやfontが軒並み表示されてなくて謎だった。

かなりレアケースではあると思うが2Railsでは内部で使われていそうな名前のRoutingは避けようという教訓を得た…。

そして、こういうときにfinalistのような意図しないoverrideを防ぐ手段があればなあと思うのであった。ただ、Ruby, Rails的には受容されない気もするが。

Routingに限らず、動的生成されるメソッドには油断しないようにしたい。


  1. 軽く覗いたところ、 sass-rails が sprockets で定義されたやつを利用しているように見える

  2. 資産(Asset)を定義する際も同様の問題が起こったとの話を聞いた

Jbuilder の generator を無効にする

故あってサンプルコードをたくさん作る必要があり、JBuilderの生成するコードがじゃまになる(けど使うので消せない)ときがあった。 そんなときに行った対策を簡単にまとめてみる。

.json.jbuilder ファイルの生成を無効化

config/application.rb に以下を記述。

    config.generators do |g|
      g.jbuilder = false
    end

経験上、他に不要なものも併せて以下のように記述することが多い。

    config.generators do |g|
      g.system_tests = nil
      g.jbuilder = false
      g.assets = false
      g.helper = false
    end

コントローラのテンプレート上書き

コントローラでは例えば以下のように、content type による振り分けが生成される。

  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

これは以下でRails標準のテンプレートが上書きされるため。

https://github.com/rails/jbuilder/blob/master/lib/generators/rails/templates/controller.rb

無効にするには、以下の手段がある。