コピペコードで快適生活

明日使えるソースを自分のために

RailsEngineを作る手順メモ

RailsEngineを作る手順を雑にメモする。
Docker/Rspecを使う前提。

docker設定

Dockerfile

FROM ruby:2.6.6

ENV NODE_VERSION 10.12.0
ENV BUNDLER_VERSION 1.17.3

ENV LANG C.UTF-8

# https://stackoverflow.com/questions/55361762/apt-get-update-fails-with-404-in-a-previously-working-build
RUN sed -i '/jessie-updates/d' /etc/apt/sources.list

RUN apt-get update -qq && apt-get install -y build-essential git mariadb-client

# Node.js
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get -y -qq install nodejs
RUN npm install -g phantomjs-prebuilt --unsafe-perm

RUN gem install bundler -v $BUNDLER_VERSION

RUN mkdir /app
WORKDIR /app


docker-compose.yml

version: '3'
services:
  mysql:
    image: mysql:5.7
    # volumes:
    #   - "./mysql-data:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: root
  app:
    build: .
    volumes:
      - "./:/app"
    ports:
      - "3000:3000"
    tty: true
    working_dir: "/app"

docker起動/ログイン

# docker起動
docker-compose up

# docker内に入る
docker exec -it my_engine_app_1 /bin/bash

(DOCKER内)プロジェクト作成

# railsインストール
gem install rails

# プロジェクトフォルダ作成
# ハイフン区切りだと階層化されるので注意
bundle exec rails plugin new my_engine --mountable -T --dummy-path=spec/dummy_app
cd my_engine

(ホスト側)gemspecを修正する

my_engine/my_engine.gemspec

# 略

# TODO欄を埋める
s.homepage    = "https://example.com/"
s.summary     = "概要を書く"
s.description = "説明を書く"

# 略

# 必要なgemを追加する
s.add_development_dependency "mysql2"
s.add_development_dependency "rspec-rails"
s.add_development_dependency "database_cleaner"
s.add_development_dependency "pry-rails"

# 略

(ホスト側)engineを修正する

lib/my_engine/engine.rb

module MyEngine
  class Engine < ::Rails::Engine
    isolate_namespace MyEngine

    # rspec使えるように追加
    config.generators do |g|
      g.test_framework :rspec
    end
  end
end

(DOCKER内)RSpec初期化

これで、spec/* ができる

bundle install
bundle exec rails g rspec:install

(ホスト側)rspecの設定ファイルを修正する

spec/rails_helper.rb

# 略

# 向き先をdummy_appに変更する
# テストはEngineをバンドルしたdummy_app内で行われる
require File.expand_path('../dummy/config/environment', __FILE__)

# 略

spec/dummy_app/config/database.yml

# dummyアプリのdbの向き先をmysqlに変更する
default: &default
  adapter: mysql2
  pool: 5
  timeout: 5000
  host: mysql
  port: 3306
  username: root
  password: root

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

production:
  <<: *default
  database: app_production

(DOCKER内)でコードジェネレートする

# モデル生成
bundle install
bundle exec rails g model xxx_logs

# ここでマイグレーションファイル修正する

# DB反映
bundle exec rake db:create
bundle exec rake db:migrate

(DOCKER内)テスト実行

# テストを実装しておく

# テスト実行
bundle exec rspec spec/

メインアプリに組み込む

Gemfileに参照先を設定して `bundle install` する

migrationファイルのコピー

bundle exec rake my_engine:install:migrations
bundle exec rake db:migrate

(補足)gem登録する

gem signin   # ~/.gem/credentialsができる
rake build   # pkgができる
rake release # gemが登録される

Rspecでcontrollerのテスト

controllerのテストをまともに書いたことなかったので。

describe TestController, type: :controller do
  let(:account) { create(:account) }

  describe 'auth' do
    context '有効なトークンを渡したとき' do
      before do
        token = TokenService.build_token(account)

        # sessionに値をセットできる
        session[:use_auth] = true

	# 指定のメソッドをparamsつきで呼ぶ
        get :auth, token: token
      end
      it '200を返す/ログインユーザが取得できる' do
        # ステータスコードをチェックできる
        expect(response).to have_http_status(200)

        # privateメソッドの値はcontroller.sendで取得できる
        current_user = controller.send :current_user
        expect(current_user.id).to eq account.id
      end
    end

    context '無効なトークンを渡したとき' do
      before do
        get :auth, token: 'xxxxxxx'
      end
      it '401を返す' do
        expect(response).to have_http_status(401)
      end
    end
  end
end

RSpecで例外チェック

これまでbegin-rescueで愚直にやってたので、もう少しシュッとした書き方を。

require 'spec_helper'

describe TokenService do
   describe :validate! do
    context '改ざんしたトークンを渡す' do
      # 処理を定義
      subject {
        token = TokenService.build_token
        TokenService.validate!(token + 'a')
      }
      it '例外発生' do
        # 処理結果としてエラーがraiseされたかをチェック
        expect { subject }.to raise_error(TokenService::InvalidTokenError)
      end
    end
  end
end

RSpecのmockの使い方

APIクライアントを外から注入できるようにして、
モックを渡してローカル環境単体でテストできようにした例。

require 'spec_helper'

class DummyService
  #
  # APIクライアントを外から指定できるようにして
  # 単体でテストできるようにする
  #
  def self.get_info(info_id, options = {})
    api_client = options[:api_client] # || DummyApiClient
    res = api_client.get_info(info_id)
    res.body
  end
end

describe DummyService do
  describe :get_info do
    let(:api_client) {
      # モックの作成
      _api_client = double("MockApiClient")

      # モックにメソッドを生やす
      allow(_api_client).to receive(:get_info).and_return(
        Struct.new(:body).new("TEST_DATA")
      )

      _api_client
    }
    it 'モックで指定した値を返す' do
      info = DummyService.get_info('test', api_client: api_client)
      expect(info).to eq 'TEST_DATA'
    end
  end
end

RSpecのletの使い方 - before&インスタンス変数使うやり方との比較

ずっとbefore&インスタンス変数でやってたので、let使うやり方をメモしておく。

require 'spec_helper'

describe 'beforeとletの違いについて' do
  context 'インスタンス変数を使う場合' do
    before do
      @account = create(:account)
    end

    it 'アカウントが存在する' do
      db_account = Account.find_by(account_id: @account.id)
      expect(db_account.present?).to eq true
    end
  end

  context 'letを使う場合' do
    # beforeと同じタイミングで、accountメソッドが作られる
    # 呼ばれたときに初期化されて、以降は同じインスタンスを返す
    let(:account) { create(:account) }

    context '一度も参照しない場合' do
      it 'アカウントは存在しない' do
        expect(Account.count).to eq 0
      end
    end

    context '一度でも参照した場合' do
      it 'アカウントは存在する' do
        db_account = Account.find_by(account_id: account.id)
        expect(db_account.present?).to eq true
        expect(Account.count).to eq 1
      end
    end
  end

  context 'let!を使う場合' do
    # 定義と同時に初期化される
    let!(:account) { create(:account) }

    context '一度も参照しない場合' do
      it 'アカウントは存在する' do
        expect(Account.count).to eq 1
      end
    end
  end
end

※参考にさせていただきました
RSpecのletを使うのはどんなときか?(翻訳) - Qiita

.ssh/configの設定メモ

よくやる書き方をコメント付きでメモ。

#
# ssh接続をタイムアウトしないための設定
# - 15秒ごとに応答確認
# - 10回応答がなかったら切断する
#
ServerAliveInterval 15
ServerAliveCountMax 10

#
# Hostにはワイルドカードを指定できる
# 設定を共有化したいときに便利
# ForwardAgentでssh-agent有効化
# StrictHostKeyCheckingでfingerPrintのチェックをスキップできる
#
Host app-prod-*
    User ec2-user
    IdentityFile ~/.ssh/app-prod.pem
    ForwardAgent yes
    StrictHostKeyChecking no

#
# fumidaiへの接続設定
#
Host app-prod-fumidai
    Hostname xx.xx.xx.xx

#
# fumidai経由による
# アプリケーションサーバへの接続設定
# -W で Hostnameで指定したホストに入出力を送ることができる
# %h:%pは ホスト名とポート番号に置換されて実行される
#
Host app-prod-batch
    Hostname xx.xx.xx.xx
    ProxyCommand ssh app-prod-fumidai -W %h:%p

#
# ローカルフォワードして接続する
# ローカルから内部API叩いたりしたいときなど
#
Host app-prod-forward
    Hostname xx.xx.xx.xx
    ProxyCommand ssh app-prod-fumidai -W %h:%p
    LocalForward 8080 prod.app.local:80