CypressOnRailsについて簡単にまとめてみた

## 目次
Cypressとは？
概要
インストール方法
使用方法（起動・実行編）
使用方法（テストコード編）
まとめ

## Cypressとは？
CypressOnRailsの導入を試みる前に、まずはCypressについて学びましょう。
Cypressは公式ドキュメントが充実しており親切にチュートリアルの動画も載せてくれてます。
この記事ではCypessとは？については省略しているので公式ドキュメントを参照ください。

- [公式ドキュメントのチュートリアル](https://docs.cypress.io/examples/examples/tutorials#Best-Practices)
- [Getting Start](https://grant-ps.blog/2018/08/10/getting-started-with-cypress-io-and-ruby-on-rails/)
- [Introduction](https://www.shakacode.com/blog/introduction-to-cypress-on-rails/)

## 概要
ざっくりまとめるとCypressの恩恵をRailsでも受けられるようになる、というgemですが
E2Eテストで陥りやすい「不安定なテスト」にならないよう以下のような仕組みが取られています。

- 各テストを行う前にdatabase_cleanerを使用する
- 各テストのデフォルトデータをseedで準備する
- factory_botを使用してデータのセットアップを行う
- 特定のテストに使用されるシナリオファイルを作成する

Rspecで使用しているテストデータを使い回しできることも、
既存のRailsアプリケーションへの導入が容易である要因の一つですね。

## インストール方法
gemを導入してからテストの実行・テスト結果を確認するダッシュボードを開くところまでとても簡単に進められます。

gemファイルに追記

```ruby:Gemfile
group :test, :development do
  gem 'cypress-on-rails', '~> 1.0'
end
```

bundle installを行い、ターミナルで以下を実行

```ruby:ターミナル
$ bin/rails g cypress_on_rails:install

# 他のフォルダで作成したいもしくはすでにcypressのフォルダがある場合
$ bin/rails g cypress_on_rails:install --cypress_folder=spec/cypress

# npmでインストールする場合
$ bin/rails g cypress_on_rails:install --install_cypress_with=npm

# すでにグローバルでインストールしている場合
$ bin/rails g cypress_on_rails:install --no-install-cypress

# すでにgenerateしたファイルをアップデートする場合
$ bin/rails g cypress_on_rails:update
```

下記のフォルダやディレクトリが作成されます
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333754/255bbc05-550a-0b6e-d928-a5883e7860f3.png" width="700">
主要ファイルのデフォルトコードを添付しておきます

```ruby:config/initializers/cypress_on_rails
if defined?(CypressOnRails)
  CypressOnRails.configure do |c|
    c.cypress_folder = File.expand_path("#{__dir__}/../../cypress")
    # WARNING!! CypressOnRails can execute arbitrary ruby code
    # please use with extra caution if enabling on hosted servers or starting your local server on 0.0.0.0
    c.use_middleware = !Rails.env.production?
    c.logger = Rails.logger
  end

  # # if you compile your asssets on CI
  # if ENV['CYPRESS'].present? && ENV['CI'].present?
  #  Rails.application.configure do
  #    config.assets.compile = false
  #    config.assets.unknown_asset_fallback = false
  #  end
  # end
end
```

```ruby:cypress/cypress_helper.rb
# This is loaded once before the first command is executed

begin
  require 'database_cleaner-active_record'
rescue LoadError => e
  puts e.message
  begin
    require 'database_cleaner'
  rescue LoadError => e
    puts e.message
  end
end

begin
  require 'factory_bot_rails'
rescue LoadError => e
  puts e.message
  begin
    require 'factory_girl_rails'
  rescue LoadError => e
    puts e.message
  end
end

require 'cypress_on_rails/smart_factory_wrapper'

factory = CypressOnRails::SimpleRailsFactory
factory = FactoryBot if defined?(FactoryBot)
factory = FactoryGirl if defined?(FactoryGirl)

CypressOnRails::SmartFactoryWrapper.configure(
    always_reload: false,
    factory: factory,
    files: [
      Rails.root.join('spec', 'factories.rb'),
      Rails.root.join('spec', 'factories', '**', '*.rb')
    ]
)
```

```js:cypress/support/on-rails.js
// CypressOnRails: dont remove these command
Cypress.Commands.add('appCommands', function (body) {
  Object.keys(body).forEach(key => body[key] === undefined ? delete body[key] : {});
  const log = Cypress.log({ name: "APP", message: body, autoEnd: false })
  return cy.request({
    method: 'POST',
    url: "/__cypress__/command",
    body: JSON.stringify(body),
    log: false,
    failOnStatusCode: false
  }).then((response) => {
    log.end();
    if (response.status !== 201) {
      expect(response.body.message).to.be.empty
      expect(response.status).to.be.equal(201)
    }
    return response.body
  });
});

Cypress.Commands.add('app', function (name, command_options) {
  return cy.appCommands({name: name, options: command_options}).then((body) => {
    return body[0]
  });
});

Cypress.Commands.add('appScenario', function (name, options = {}) {
  return cy.app('scenarios/' + name, options)
});

Cypress.Commands.add('appEval', function (code) {
  return cy.app('eval', code)
});

Cypress.Commands.add('appFactories', function (options) {
  return cy.app('factory_bot', options)
});

Cypress.Commands.add('appFixtures', function (options) {
  cy.app('activerecord_fixtures', options)
});
// CypressOnRails: end

// The next is optional
// beforeEach(() => {
//  cy.app('clean') // have a look at cypress/app_commands/clean.rb
// });

// comment this out if you do not want to attempt to log additional info on test fail
Cypress.on('fail', (err, runnable) => {
  // allow app to generate additional logging data
  Cypress.$.ajax({
    url: '/__cypress__/command',
    data: JSON.stringify({name: 'log_fail', options: {error_message: err.message, runnable_full_title: runnable.fullTitle() }}),
    async: false,
    method: 'POST'
  });

  throw err;
});
```

### database.ymlをアップデートする
テストを実行する度にdevelop環境のデータベースが汚れるのを防ぐにはdatabase.ymlを編集しましょう

```ruby:database.yml
development:
  <<: *default
  database: <%= ENV['CYPRESS'] ? 'my_db_test' : 'my_db_development' %>
test:
  <<: *default
  database: my_db_test
```

## 使用方法（起動・実行編）
ローカル環境で起動までを行なってみましょう

```ruby
# railsをスタートさせる
$ bin/rails server -p 5017

# 別ウィンドウでcypressを起動する
$ yarn cypress open 

# npmを使用する方はこちら
$ node_modules/.bin/cypress open 

# spec/cypressにフォルダを変えた方はこちら
$ yarn cypress open --project ./spec
```
cypressを起動すると、このような画面に移ります
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333754/093e90fb-2b99-89c3-b90f-c9325df0bf42.png" width="800">

「Log In to DashBord」からログインすると、このようなページへリダイレクトします
GUIでテスト結果をデバックできるのは分かりやすいですね
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333754/b395d62e-0fad-60ac-67e8-3ab90ae8243b.png" width="800">


### 実行

```ruby:ターミナル
# railsを起動しバックグランドでサーバーを起動する
$ yarn run cypress run

# npmを使用する方はこちら
$ node_modules/.bin/cypress run 
```

テスト結果はこのように表示されます（何も設定していないのでテスト落ちてます）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333754/16fa0cd3-0118-e662-d321-15f674d6008c.png" width="800">


## 使用方法（テストコード編）
### factory botの使用例

既存アプリケーションで作成していたfactory_botを直接実行することもできます

```js:spec/cypress/integrations/simple_spec.js
describe('My First Test', function() {
  it('visit root', function() {
    // バックエンドを呼び出し、アプリケーションの状態を準備する
    cy.appFactories([
      ['create_list', 'post', 10],
      ['create', 'post', {title: 'Hello World'} ],
      ['create', 'post', 'with_comments', {title: 'Factory_bot Traits here'} ] # traitsを使う
    ])

    // テスト対象のアプリケーションにアクセスする
    cy.visit('/')

    cy.contains("Hello World")

    // アクセス結果
    cy.appFactories([['create', 'invoice', { paid: false }]]).then((records) => {
     cy.visit(`/invoices/${records[0].id}`);
    });
  })
})
```

### テストフィクスチャーの使用例
`activerecord_fixtures.rb`はデフォルトで作成されるファイルです。

```ruby:spec/cypress/app_commands/activerecord_fixtures.rb
require "active_record/fixtures"

fixtures_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
fixture_files = Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] }

logger.debug "loading fixtures: { dir: #{fixtures_dir}, files: #{fixture_files} }"
ActiveRecord::FixtureSet.reset_cache
ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
```

実際の使用してみましょう。

```js:spec/cypress/integrations/simple_spec.js
describe('My First Test', function() {
  it('visit root', function() {
    // This calls to the backend to prepare the application state
    cy.appFixtures()

    // Visit the application under test
    cy.visit('/')

    cy.contains("Hello World")
  })
})
```

### シナリオの使用例
シナリオとはテストで参照できる名前付きブロックです。
シナリオは`spec/cypress/app_commands/scenarios`ディレクトリで定義してください。

```ruby:spec/cypress/app_commands/scenarios/basic.rb
Profile.create name: "Cypress Hill"

# factory_botがあればcypress_helperを使うことができます
CypressOnRails::SmartFactoryWrapper.create(:profile, name: "Cypress Hill")
```

次に、テストでシナリオを参照します。

```js:spec/cypress/integrations/scenario_example_spec.js
describe('My First Test', function() {
  it('visit root', function() {
    // This calls to the backend to prepare the application state
    cy.appScenario('basic')

    cy.visit('/profiles')

    cy.contains("Cypress Hill")
  })
})
```
### seedの使用例
`spec/cypress/app_commands`ディレクトリにファイルを作成してください

```js:spec/cypress/app_commands/load_seed.rb
load "#{Rails.root}/db/seeds.rb"
```
次に、テストでcy.app('load_seed')を参照します。

```ruby:spec/cypress/integrations/simple_spec.js
describe('My First Test', function() {
  beforeEach(() => { cy.app('load_seed') })

  it('visit root', function() {
    cy.visit('/')

    cy.contains("Seeds")
  })
})
```

## まとめ
E2Eテストといえばseliniumが有名ですが、最近有名企業もseliniumからCypressに移行した話などが増えているみたいです。
フレームワークやラッパーの選定が面倒なseliniumに対してセットアップが容易なCypressは今後もシェアを広げていきそうです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333754/5e97bd3e-d4c5-c714-3f36-b5f25e217682.png" width="800">

RailsアプリケーションだとSystem Specがありますが、CypressOnRailsも使いやすそうなので是非触ってみてください