Docker 上で実行した feature spec をデバッグする方法

# 概要

Docker + Selenium + Headless Chrome な環境で feature spec を実行する時に、ブラウザの開発者ツールを使ってデバッグしたかったので調べた。

こんな感じで使える↓

![Screen Shot 2018-02-28 at 1.19.56.png](https://qiita-image-store.s3.amazonaws.com/0/89649/f528d6f1-190b-5114-036d-e9069f12489d.png)

サンプルコードはこちら
https://github.com/yszk0123/selenium-docker-rails-example

今回の例では Chrome を使ったが、Firefox も可能なはず。

# 検証環境

- macOS (High Sierra 10.13.3)
- Docker for Mac (18.02.0-ce-mac53)
- Docker
    - [ruby](https://hub.docker.com/_/ruby) (2.5.0-alpine)
    - [selenium/standalone-chrome-debug](https://hub.docker.com/r/selenium/standalone-chrome-debug/) (3.9.1-actinium)
- Gem
    - rails (5.1.5)
    - rspec-rails (3.7.2)
    - [capybara](https://github.com/teamcapybara/capybara) (2.18.0)

# やり方

ブラウザ・Selenium サーバー・[VNC](https://ja.wikipedia.org/wiki/Virtual_Network_Computing) サーバーなどが同梱された[selenium/standalone-chrome-debug](https://hub.docker.com/r/selenium/standalone-chrome-debug/) という docker イメージを使う[^docker-selenium]。
Docker for Mac には
 VNC クライアントが同梱されているので[^vnc-docker-for-mac]、特別な設定なしにコンテナの外からブラウザを操作できる。

今回は次の２つのコンテナに分ける。

1. テストを実行するコンテナ `app`
2. Selenium サーバー用のコンテナ `chrome`

また、テスト用に以下のホスト・ポートを使用する (docker-compose のネットワーク機能を活用)。

- アプリケーションサーバー `http://app.com:3000`
- Selenium サーバー `http://chrome.com:4444`
- VNC サーバー `http://localhost:5900` (ホストマシンからアクセス)

## docker-compose の設定

```docker-compose.yml
version: "2"
services:
  # テストを実行する Rails サーバー。Dockerfile は以下のリンク
  # https://github.com/yszk0123/selenium-docker-rails-example/blob/master/Dockerfile
  app:
    container_name: app
    build: .
    ports:
      - 3000:3000
    volumes:
      - .:/app
    networks:
      test-network:
        aliases:
          - app.com
  chrome:
    container_name: chrome
    image: selenium/standalone-chrome-debug:3.9.1-actinium
    ports:
      - 4444:4444
      - 5900:5900
    networks:
      test-network:
        aliases:
          - chrome.com
networks:
  test-network:
```

## RSpec (Capybara) の設定

```spec/rails_helper.rb
require 'rspec/rails'
require "capybara/rails"
require "selenium/webdriver"

if ENV["LAUNCH_BROWSER"]
  Capybara.configure do |config|
    # docker-compose で設定した alias を使い
    # chrome コンテナ側から app コンテナ内のサーバーを参照
    config.server_host = "app.com"
    # ポートはデフォルトではランダムに割り当てられるが、設定を簡単にするため固定
    config.server_port = 3000
    config.javascript_driver = :selenium_chrome_headless
  end

  Capybara.register_driver :selenium_chrome_headless do |app|
    Capybara::Selenium::Driver.new(
      app,
      # 別コンテナで動かしている Selenium のリモートサーバーを操作
      browser: :remote,
      # Chrome 以外は適宜設定を変える
      desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
        chromeOptions: {
          args: [
            "window-size=1024,512",
          ]
        }
      ),
      url: "http://chrome.com:4444/wd/hub",
    )
  end
end
```

## 実行方法

```sh
$ docker-compose up -d
# VNC クライアントを起動
$ open vnc://localhost:5900
# ここでパスワードを求められるので secret と入力
# テストを実行
$ docker exec -it app sh -c 'LAUNCH_BROWSER=true bundle exec rspec'
```

後は止めたいところに `pry.binding` などを仕込めばOK。

```spec/features/sample_spec.rb
require "rails_helper"

RSpec.feature "Sample", js: true do
  scenario "sample" do
    visit sample_path

    binding.pry

    expect(page).to have_content("Hello, world")
  end
end
```

# おまけ (JavaScript でテストを書く場合)

サーバーサイドは Rails だけど JavaScript でテストを書くケースもありそうだったので軽く調べた。
以下は `selenium-webdriver` を使った例。

```sh
$ docker-compose run --rm app sh -c 'bin/rails s'
$ docker-compose run --rm chrome
$ docker-compose run --rm node
```

```diff:docker-compose.yml
+  node:
+    container_name: node-test
+    image: node:9.6.1-alpine
+    command: sh -c 'cd /app && yarn init -y && yarn add selenium-webdriver && node test.js'
+    volumes:
+      - ./test.js:/app/test.js
+    networks:
+      test-network:
   chrome:
     container_name: chrome
```

```js:test.js
const { Builder } = require('selenium-webdriver');

function wait(delay) {
  return new Promise(resolve => setTimeout(resolve, delay));
}

(async () => {
  const driver = new Builder()
    .forBrowser('chrome')
    .usingServer('http://chrome.com:4444/wd/hub')
    .build();

  try {
    await driver.get('http://app.com:3000/sample');
    await wait(3000);
  } finally {
    await driver.quit();
  }
})();
```

# おわりに

Rails と Selenium にはあまり詳しくないため、間違いがあればご指摘いただけると助かります。

# 参考

- [RubyとSeleniumでHeadless chromeを動かす on Ubuntu/Linux](https://qiita.com/meguroman/items/41ca17e7dc66d6c88c07)
- [Capybaraを使う際に知っておきたいこと](https://qiita.com/take/items/779747e0981355e569ad#js%E5%81%B4%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E3%82%92%E3%81%97%E3%81%9F%E3%81%84)

[^docker-selenium]: [docker\-selenium 使って見た](https://qiita.com/yutaChaos/items/4a1da5d55a3bf0df889e)
[^vnc-docker-for-mac]: https://github.com/SeleniumHQ/docker-selenium/blob/3.9.1-actinium/README.md#debugging