Rails アプリにモデルスペックを導入する

f:id:yazawa_tech:20190829065135p:plain

はじめに

今回はRails でアプリケーションを作っている際に、モデルスペックを書くシーンがあったので、「どんなテストを書けば良いのか」という観点でまとめてみた。

目次

参考書籍

今回の記事はこちらの書籍を参考にまとめた (なるべく内容を丸写ししないようにまとめたつもりですが、ご指摘などあればコメントいただけると幸いです)

leanpub.com

モデルスペックを導入する時の手順

  1. 既存のモデルに対してモデルスペックを作る
  2. モデルのバリデーション、クラスメソッド、インスタンスメソッドのテストを書く

モデルスペックに含めるべきテスト

  1. モデルの状態が有効であること(有効なパラメータを与えて初期化等)
  2. モデルの状態が有効でないこと(Not Null などのバリデーションが失敗するようなデータ等)
  3. クラスメソッドとインスタンスメソッドが期待通りに動作すること

一言で言うと、「モデルが有効か無効か」と「各メソッドが期待通りの動きをすること」のテストを記述する。

モデルスペックの書き方

モデルスペックの大枠は以下のようになる。例えばEC サイトで製品を表すproduct モデルがあった場合、以下のようになる。

describe Product do
  it 'is valid with name, price' # 有効な場合のテスト
  it 'is invalid withuot name'   # 有効でない場合のテスト
  it 'is invalid withuot price'  # 有効でない場合のテスト
end
  • describe から始まり、Product モデルへのテストを記述することを宣言している
  • it の後に説明文を書くことで、何に対しての検証か、何を期待するかが明確になっている
  • 【重要】describe の後に続く「モデル名」 + it の後に続く「テストケース」が英語の文章になり、意味が通じるように記述する
    • Product is valid with name, price;

上の状態で bin/rspec コマンドを実行すると、 pending の状態のテストが3つ出来上がる。

1.モデルが有効な場合のテスト

有効であることを検証するためには、例えば以下のようにテストを記述する。

describe Product do
  it 'is valid with name, price' # 有効な場合のテスト
    product = Product.new(
      name: "spoon",
      price: 300
    )
    expect(product).to be_valid
  end
end

expect(product).to be_validproduct.valid? == true を検証しているイメージを持つと、わかりやすい。

2.モデルが有効でない場合のテスト

有効でない場合のテスト、すなわちバリデーションエラーになるようなテストを実行したい場合には、例えば以下のように記述すれば良い。

describe Product do
  it 'is invalid withuot name' do  # 有効でない場合のテスト
    product = Product.new(
      name: nil,
      price: 300
    )
    product.valid?
    expect(product.errors[:name]).to include("can't be blank")
  end
end

product.errors[:name] は 「Product モデルのname 属性に関するエラーメッセージの配列」を返し、その配列が 「"can't be blank"という文字列を含んでいるか」を検証している。product.errors[:name].include?("can't be blank") == true を検証しているイメージを持つと、わかりやすい。

3.クラスメソッドとインスタンスメソッドが期待通りに動作するテスト

インスタンスメソッドのテスト

例えばインスタンスメソッドに以下のようなメソッドがあったとする。

class Product
  def equals?(product)
    (self.name == product.name) && (self.price == product.price)
  end
end

この場合は、モデルが有効な場合や無効な場合と同様に以下のように書ける。

describe Product do
  it 'returns true given product with same name and same price'
    one_product = Product.new(
      name "banana",
      price 500
    )
    another_product = Product.new(
      name "banana",
      price 500
    )
    expect(one_product.equals?(another_product)).to eq true
  end
end

スコープとクラスメソッドのテスト

例えば、製品を金額で条件を指定して検索するスコープが以下のように定義されているとする

scope :upper_than, ->(price) {
  where("price >= ?", price)
}

この場合にモデルのテストを書くとしたら以下のようになる(大枠のみ)

RSpec.describe Product, type: :model do
  it "returns products that match the search price"
    # Product オブジェクトをいくつか作る
    # 検索条件を渡してスコープを実行し、一致するオブジェクトを含むか含まないかをテストする
    # …
    # 失敗するテストも追加する
    # 例えば、検索条件として不適切な値を入れた時に、検索結果が0件の場合のテストなど
end

まとめ

  • 今回はRSpec を使った書き方も含め、モデルスペックを書くときの指標をまとめてみた
  • Rails だけじゃなく、全ての単体テストを書く時にも参考になると思うので、他のアプリでも指標として使いたい
  • システムスペックの指標も自分なりに整理したいので、次回はシステムスペックについて掘り下げてみたいと思う

複数コミットをgit rebaseでまとめる方法

f:id:yazawa_tech:20190821161637p:plain

はじめに

今回はチーム開発でモダンな現場であればどこでも使われているGitrebase コマンドを使ったケーススタディを紹介する。

こんな時に使いたい

  • pushする前に自分のローカルでコミット履歴を綺麗にしたい
  • コミットする単位を間違えた

使用するコマンド

  • git log --oneline
  • git rebase -i [commit]

実行手順

1.コミットログを確認する

git log --oneline

# e4bcc8a (HEAD -> feature/branch_name) commit-3
# 530f8a2 commit-2
# ed44043 commit-1
# 702080b (develop) xxxxx

2.コミットログのうち、まとめたい履歴のひとつ前のコミットIDを指定して以下のコマンドを実行

git rebase -i

3.まとめる対象のコミットログを選択

vimの編集画面が開く

pick ed44043 commit-1
pick 530f8a2 commit-2
pick e4bcc8a commit-3

# Rebase 702080b..e4bcc8a onto 702080b (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log messag
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to b
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

まとめたいコミットを選択し、'pick'を's'に変える

pick ed44043 commit-1    # このコミットログにまとめられる
s 530f8a2 commit-2       # ↑のコミットログにまとめられる
s e4bcc8a commit-3       # ↑のコミットログにまとめられる

# この状態で :wq

コミットコメントをまとめる

before
# This is a combination of 3 commits.
# This is the 1st commit message:

commit-1

# This is the commit message #2:

commit-2

# This is the commit message #3:

commit-3

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Tue Feb 26 12:47:39 2019 +0900
#
# interactive rebase in progress; onto 702080b
# Last commands done (3 commands done):
after
# This is a combination of 3 commits.
# This is the 1st commit message:

Three commits

# This is the commit message #2:

# commit-2

# This is the commit message #3:

# commit-3

上記の状態で :wq を実行すると、以下のメッセージが出現

$ git rebase -i 702080b
    :
Successfully rebased and updated refs/heads/feature/branch_name.

5.ログを確認

$ git log --oneline
# 058cabb (HEAD -> feature/branch_name) Three commits
# 702080b (develop) xxxxx

### before
# e4bcc8a (HEAD -> feature/branch_name) commit-3
# 530f8a2 commit-2
# ed44043 commit-1
# 702080b (develop) xxxxx

これで複数コミットを綺麗にまとめることができた。

まとめ

  • Git のコミットログを綺麗に保つための rebase -i コマンドを紹介した
  • ローカルホストではこまめにコミットしていたけど、レビューを出すときなどにコミットが大量にあると、レビュアーが見にくくなる場合がある
  • レビュアーとしてもコミットがまとまっているとやりやすいシーンがあるが、コミットを適切にまとめることでコミットツリーでも履歴が追いやすくなるので、積極的に使っていきたい

参考サイト

Rails でプログラミングする時に意識しておきたいこと

f:id:yazawa_tech:20190430175836p:plain

はじめに

今回はRails でWeb アプリケーションを作る時に、常に意識しておくと良い点をまとめておく。

scaffold で生成されたコードをなるべく維持したまま機能を実現する

コントローラーを経由して画面を表示する際に、 scaffold に近い形でコントローラーを保っておき、 モデルに処理を寄せながら機能を実現することで、よりRails のレールに乗った形で実装ができる。

例えばscaffold で生成した users_controllerソースコードは以下のようになっている。

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    @users = User.all
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = User.new
  end

  # GET /users/1/edit
  def edit
  end

  # 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

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def user_params
      params.fetch(:user, {})
    end
end

コントローラーの責務はあくまでパラメーターを使用してモデルを参照すること がメインの目的なので、「param を引数に伴わない処理やメソッド呼び出しなど」はなるべくモデルに寄せていく。

ビューで複雑な処理を書くような場合にはヘルパーメソッドを用意する

実装の際に気をつけるべきことの1つ目の方針は、Rails「Skinny Controller, Fat Model」という考え方に基づくものであり、「コントローラーをスッキリさせてビジネスロジックはなるべくモデルに実装する」というものである。これと同様にビューでRuby のコードを利用したビジネスロジックをゴリゴリ書くときは、ヘルパーメソッドに切り出すということも意識しておきたい。

ビジネスロジックをヘルパーメソッドに切り出す時に意識すべき点は、「3~4行以上になること」または「再利用性があること」を目安に切り出していくと良い。

ビューに表示するオブジェクトの情報は一つにするのがベター

ビューに情報を返したり、特にフォームであるオブジェクトのパラメータを送信したりする場合には、なるべく1つのビューに対して1つのオブジェクトのフォームを用意する方が、Rails の得意な形で実装できる。

ビジネス要件などでどうしても1つのビューに複数のオブジェクトに対するフォームを実装しなければならない場合などは、Ajax 化してしまうのが良い。その場合はJavaScript で処理をすることになるが、コードがよりスッキリ書ける。

まとめ

  • Rails で実装する際に何度でも意識しておきたいことをまとめた
  • Rails 初心者の人にとっては、これを意識するだけでもだいぶ変わってくると思う
  • まだまだ大事なことはあるが、とりあえず優先度の高いものをまとめて実装をする時の指針にしたい

小さなことを習慣化しよう!「TIL」を始めてみた

f:id:yazawa_tech:20190804172105j:plain

今週から実施しているTIL という自分のための習慣についてメモしておく。

TIL とは

これは同じ会社の人が実践してみようということで初めて認知した単語だったけど、自分としても「いい習慣化」のための一助になりそうだったので、良いと思った点をまとめておく。

実践してみて感じたメリット

1.学びが蓄積されていく

プログラミングをやっていると、日々Google で検索したり書籍で調べたり、細かいことで調べ物をすることってたくさんあると思う。それを小さなアウトプットとしてメモしてコミットしておくことで、自分だけのメモ帳みたいなのが蓄積されていく。かつ、Markdown でまとめることで、少なからず情報を整理しようという心理も働くので、知識を整理するのに役に立つ。知識を整理しようとすると、その場限りでインプットをするよりもはるかに知識としての定着が図れる。

2.後から検索できる

「いつも調べているけどいつも忘れちゃう」などといったものは自分のリポジトリにメモしてコミットしておくことで、自分だけのナレッジベースとして後から検索できる。例えば何か調べ物をするときにであっても、「こっちの情報とこっちの情報を合わせたらできた」とか、「検索したときには出てこなかったエラーが出た」時などにも自分でまとめてメモしておけるので、後から検索したときに生きた情報として活用できることが期待できる。さらに、GitHub の対象のリポジトリをPublic にしておけば、自宅でまとめた情報を会社で閲覧するといったことも可能。

3.簡単に実践できる

「誰かに見せるもの」ではなく、あくまでも「自分用のメモ」として残していく(ことをオススメする)ので、何を書いてもいいし、どんなに小さなことでもいい。小さな習慣を続けていくと、自分の中に成功体験を積み重ねられるし、小さな習慣を実践していると、だんだんと大きな習慣になっていく。(これについては書籍『小さな習慣』にも書いてあるので読むことをおすすめ)

小さな習慣

小さな習慣

4.続けていて気持ちがいい

何かを毎日続ける、というのは大人になると結構大変だと感じているが、先に書いたように「簡単に実践できる」ものであれば続けやすいし、GitHub には自分のコントリビューションを可視化できる画像があるため「自分はこれだけ継続しているんだ」というのが実感でき、自信にも繋がりやすい。

f:id:yazawa_tech:20190804172259p:plain
通称"草"

5.スキルマップにもなる

例えば「rails」という名前をディレクトリを作成し、その中にgem についてのメモを蓄積していく。それを他の技術についても同様に実践する。この習慣を継続していれば、どの技術について自分が学んできたかが可視化できるようになる。加えて「最終更新日」や「ファイルサイズ」などを見ると、いつ学んだか、どれだけの量を蓄積しているか、などもわかるので、不足している分野の再学習や情報の更新などの目安にもなりやすい。

f:id:yazawa_tech:20190804172412p:plain
プログラミング言語ミドルウェアフレームワークの名前が並ぶ

まとめ

  • TIL というGitHub の文化について紹介した
  • 小さな習慣を続けることがスキルアップへの一番の近道だと思っているので、この習慣は続けていきたい
  • 情報の丸写しなどをPublic リポジトリでアップしていると、著作権違反になったり作者の人に失礼にあたるので気をつけよう

Rails で複雑なレコード検索処理を簡単に組み込める『Ransack』の紹介

f:id:yazawa_tech:20190728134048p:plain

はじめに

Rails アプリケーションでよく使用されるgem の使い方や導入方法をまとめたく、手始めにRansack について、簡単な入門記事をまとめてみる。今回記述したソースコードは以下のリポジトリにプッシュしてある。

github.com

Ransack とは

Rails アプリケーション用に単純な検索フォームと高度な検索フォームの両方を作成できるgem である。

github.com

使用方法としてはSimple ModeAdvanced Mode があるようだが、今回はSimple Mode の使用方法を簡単にまとめる。

入門

gem のインストール

Gemfile に以下の記述を追加して、bundle コマンドでインストールする。

gem 'ransack'
$ bundle

Simple Mode で使う

フォームの作成

もともとRansack がMetaSearch というgem の書き換えとして作られており、シンプルモードはMetaSearch に精通している人であればすぐに導入できるようだ。 現在サンプル用に作成しているタスク管理アプリに実装してみる。

app/views/tasks/index.html.slim

= search_form_for @q, class: 'mb-5' do |f|
  .form-group.row
    = f.label :name_cont, '名称', class: 'col-sm-2 col-form-label'
    .col-sm-10
      = f.search_field :name_cont, class: 'form-control'
  .form-group
    = f.submit class: 'btn btn-outline-primary'

まずview にsearch_form_for というヘルパーメソッドを使ってRansack 用の検索フォームを作る。q というオブジェクトに name_cont というパラメータを設定してサーバーサイドに送信するように設定。_cont というサフィックスは、contains の略で、検索するときに対象の文字列を含むものを検索してくれる。

パラメータの処理

app/controllers/tasks_controller.rb

def index
  @q = current_user.tasks.ransack(params[:q])
  @tasks = @q.result(distinct: true).recent
end

コントローラ側ではq パラメータを受け取り、Task モデルに組み込まれたransack メソッドを使用してRansack::Search クラスのオブジェクトを生成している。以下は ransack メソッドの中身。

module Ransack
  module Adapters
    module ActiveRecord
      module Base
      # 一部省略
      def ransack(params = {}, options = {})
        ActiveSupport::Deprecation.warn("#search is deprecated and will be removed in 2.3, please use #ransack instead") if __callee__ == :search
        Search.new(self, params, options)
      end

生成したRansack::Search のメソッドである result メソッドを呼び出して@tasks を取得している。オプションとしてdistinct: true を渡すと、重複したデータを除外してセレクトしてくれる。重複しているかのデータの判断は、抽出対象の全てのカラムが一致しているかなので、カラムが複数対象であれば複数のカラムの値が一致していた場合、カラムが単一カラムであれば、そのカラムの値が一致していた場合に除外対象となる。

recent は今回のサンプルアプリに定義してあるscope で、task.rb に以下のように記述してある。

  scope :recent, -> {order(created_at: :desc)}

これによってカスタムクエリ用のメソッドとして、SQLの実行時に ORDER BY "tasks"."created_at" DESC が付与されるようになる。

デモ

これだけで基本的な検索フォームが作成できるので、試しに動かしてみる。

f:id:yazawa_tech:20190728174029p:plain
タスクの一覧画面

f:id:yazawa_tech:20190728174119p:plain
検索するキーとして「設計」と入力して検索ボタンをクリックしたあと

無事にタスクの名称に検索キーワードを含むものだけを表示することができた。検索ボタンをクリックした時のアプリケーションログは以下のように出力されている。

Started GET "/tasks?utf8=%E2%9C%93&q%5Bname_cont%5D=%E8%A8%AD%E8%A8%88&commit=%E6%A4%9C%E7%B4%A2" for ::1 at 2019-07-28 15:09:45 +0900
Processing by TasksController#index as HTML
  Parameters: {"utf8"=>"", "q"=>{"name_cont"=>"設計"}, "commit"=>"検索"}
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 4], ["LIMIT", 1]]
  ↳ app/controllers/application_controller.rb:8
  Rendering tasks/index.html.slim within layouts/application
  Task Load (0.3ms)  SELECT DISTINCT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 AND "tasks"."name" ILIKE '%設計%' ORDER BY "tasks"."created_at" DESC  [["user_id", 4]]
  ↳ app/views/tasks/index.html.slim:22
  Rendered tasks/index.html.slim within layouts/application (7.7ms)
Completed 200 OK in 45ms (Views: 39.3ms | ActiveRecord: 1.9ms)

これで簡単に検索フォームを実装することができた。

ソート機能の付与

Ransack ではsort_link というヘルパーを使用することで、テーブルヘッダーをソート可能なものにすることができる。

app/views/tasks/index.html.slim

:
.mb-3
table.table.table-hover
  thead.thead-default
    tr
      th= sort_link(@q, :name)
      th= sort_link(@q, :created_at)
      th
      th
:

上記のコードではname(名称) とcreated_at(作成日時)をソート可能にしている。

デモ

f:id:yazawa_tech:20190728182540p:plain
「新規」というキーワードで検索した結果

f:id:yazawa_tech:20190728182659p:plain
検索結果の表示部分の「名称」ヘッダーをクリック。ヘッダー文字の横に▲が付いている。

f:id:yazawa_tech:20190728182857p:plain
もう一度クリックすると、▼に変わり、降順になる。

f:id:yazawa_tech:20190728182955p:plain
「登録日時」ヘッダーも同様にクリックすると昇順になる。

f:id:yazawa_tech:20190728183033p:plain
もう一度クリックすると降順になる。

まとめ

  • 今回はRails アプリの開発でよく使われるgem の入門手順を書いた
  • よく使われているgem はGitHub のREADME.md やWiki が充実しているので、初めて使う場合でもわかりやすい
  • Advanced Mode やもっと複雑な検索もできるようなので、次はそれを試したい

CircleCI でリモートリポジトリにプッシュした時にRSpec を実行する

f:id:yazawa_tech:20190720171857p:plain

今回はCircleCI でリモートリポジトリにプッシュしたら自動でテスト(RSpec)を実行するための設定をしてみた。その時の知見やyaml ファイルの書き方をまとめておく。

設定ファイル

.circleci/config.yml は以下のように記述した。

version: 2.1

jobs:
  build:
    docker:
      - image: circleci/ruby:2.5-stretch-node-browsers-legacy
        environment:
          BUNDLE_JOBS: 3
          BUNDLE_RETRY: 3
          BUNDLE_PATH: vendor/bundle
          BUNDLER_VERSION: 2.0.1
          PGHOST: 127.0.0.1
          PGUSER: circleci
          RAILS_ENV: test
      - image: circleci/postgres:11-alpine-postgis-ram
        environment:
          POSTGRES_USER: circleci
          POSTGRES_DB: taskleaf
          POSTGRES_PASSWORD: password
    working_directory: ~/taskleaf
    steps:
      - checkout
      - run:
          name: setup bundler
          command: |
            sudo gem update --system
            sudo gem uninstall bundler
            sudo rm -f /usr/local/bin/bundle
            sudo rm -f /usr/local/bin/bundler
            sudo gem install bundler
      - run: bundle install
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      - run: bundle exec rspec

タグの書き方について、各キーワードと、何をしているかを以下にまとめておく。

解説

version: CircleCI のバージョンを指定する

version: 2.1

version キーには、使用するCircleCI のバージョンを指定する。現在は2, 2.0, 2.1 のうちのどれかを指定。ここに指定したバージョンが将来的に非推奨になったり、大きな変更があった場合に警告を出すかどうかの判断に使われるようだ。

jobs: ジョブの集合を表す

jobs:
  build:

CircleCI では実行ステップをjob という単位で区切っていて、jobs 配下に各ジョブ名を記述していく。今回は build というジョブのみ定義してある状態。Workflows というジョブの実行を制御できる仕組みがあるが、今回はこの設定は使用しないので割愛。

circleci.com

このWorkflows の設定を行わない場合、jobs の中に必ず build という名前のジョブを定義する必要があるので注意。build の他にも任意の名前のジョブを定義することができる。

docker: ジョブ実行の際にDocker Executor を使う

ジョブの実行にはExecutor を使い、現在はdocker, machine, macos の3種類から選んで使用することができる。

circleci.com

  build:
    docker:
      - image: circleci/ruby:2.5-stretch-node-browsers-legacy
        environment:
          BUNDLE_JOBS: 3
          BUNDLE_RETRY: 3
          BUNDLE_PATH: vendor/bundle
          BUNDLER_VERSION: 2.0.1
          PGHOST: 127.0.0.1
          PGUSER: circleci
          RAILS_ENV: test
      - image: circleci/postgres:11-alpine-postgis-ram
        environment:
          POSTGRES_USER: circleci
          POSTGRES_DB: taskleaf
          POSTGRES_PASSWORD: password

docker キーに指定できるオプションの中で、今回は必須であるimage と任意のオプションenvironment を指定している。image では使用するDocker イメージを指定し、 environment ではコンテナ内部で使用する環境変数マッピングを定義している。

今回は BUNDLER_VERSION: 2.0.1 を記述している。対象のアプリのRuby のバージョンが 2.5.5 であり、後のジョブのステップ実行の中でbundler を使うシーンがあるのだが、bundler のバージョンを指定しないでジョブを実行した場合、以下のようなエラーが出たためバージョン指定をしている。

$ #!/bin/bash -eo pipefail
bundle exec rake db:create
You must use Bundler 2 or greater with this lockfile.
Exited with code 20

使用するDocker イメージについてはCircleCI が提供しているビルド済みイメージがDocker Hub に公開されている。ビルド済みイメージについてのドキュメントは以下。

circleci.com

docker キーに指定できるオプションは以下に詳しい記述がある。

circleci.com

working_directory: steps の実行場所を指定する

    working_directory: ~/taskleaf

この後に記述されているsteps を実行するディレクトリを指定する。もしディレクトリが存在しない場合には、自動で作成される。

steps: 実行する内容を列挙する

    steps:
      - checkout
      - run:
          name: setup bundler
          command: |
            sudo gem update --system
            sudo gem uninstall bundler
            sudo rm -f /usr/local/bin/bundle
            sudo rm -f /usr/local/bin/bundler
            sudo gem install bundler
      - run: bundle install
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      - run: bundle exec rspec

steps の後には実行したいコマンドなどを列挙していき、ジョブの内容を組み立てていく。 checkout コマンドはソースコードworking_directory で指定したディレクトリにチェックアウト(=git clone のようなもの)するためのヘルパー関数。試しにstepscheckout の記述の後に以下のような記述を足してみる。

    steps:
      - checkout
      - run: pwd
      - run: ls -la
      - run: git remote -v

こうした場合にCircleCI のダッシュボード画面でコマンドの実行結果を見てみる。

f:id:yazawa_tech:20190721164529p:plain

### pwd の実行ログ
$ #!/bin/bash -eo pipefail
pwd
/home/circleci/taskleaf

### ls -la の実行ログ
$ #!/bin/bash -eo pipefail
ls -la
total 100
drwxr-xr-x 15 circleci circleci 4096 Jul 21 07:38 .
drwxr-xr-x  6 circleci circleci 4096 Jul 21 07:38 ..
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 .circleci
drwxr-xr-x  8 circleci circleci 4096 Jul 21 07:38 .git
-rw-r--r--  1 circleci circleci  620 Jul 21 07:38 .gitignore
-rw-r--r--  1 circleci circleci   22 Jul 21 07:38 .rspec
-rw-r--r--  1 circleci circleci    6 Jul 21 07:38 .ruby-version
-rw-r--r--  1 circleci circleci 2314 Jul 21 07:38 Gemfile
-rw-r--r--  1 circleci circleci 6882 Jul 21 07:38 Gemfile.lock
-rw-r--r--  1 circleci circleci  374 Jul 21 07:38 README.md
-rw-r--r--  1 circleci circleci  227 Jul 21 07:38 Rakefile
drwxr-xr-x 10 circleci circleci 4096 Jul 21 07:38 app
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 bin
drwxr-xr-x  5 circleci circleci 4096 Jul 21 07:38 config
-rw-r--r--  1 circleci circleci  130 Jul 21 07:38 config.ru
drwxr-xr-x  3 circleci circleci 4096 Jul 21 07:38 db
drwxr-xr-x  4 circleci circleci 4096 Jul 21 07:38 lib
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 log
-rw-r--r--  1 circleci circleci   66 Jul 21 07:38 package.json
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 public
drwxr-xr-x  4 circleci circleci 4096 Jul 21 07:38 spec
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 storage
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 tmp
drwxr-xr-x  2 circleci circleci 4096 Jul 21 07:38 vendor

### git remote -v のログ
$ #!/bin/bash -eo pipefail
git remote -v
origin  git@github.com:yazawatech/taskleaf.git (fetch)
origin  git@github.com:yazawatech/taskleaf.git (push)

pwd コマンドのログにはworking_directory で指定したディレクトリが出力され、ls -la コマンドのログにはRails アプリのディレクトリ構造が出力され、git remote -v のログでそのディレクトリ構造のファイル群がクローンされたリポジトリが表示されている。

run: 実行するコマンドを記述

      - run:
          name: setup bundler
          command: |
            sudo gem update --system
            sudo gem uninstall bundler
            sudo rm -f /usr/local/bin/bundle
            sudo rm -f /usr/local/bin/bundler
            sudo gem install bundler
      - run: bundle install
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      - run: bundle exec rspec

複数のrun キーを列挙することで、上から順にコマンドを実行することができる。今回の目標は最後のbundle exec rspec を実行し、テストケースが全てパスすること。ここまでエラーなどがなく最後までジョブの実行が完了すると、ダッシュボードで以下の画面のようにsuccess のタグが付く。

f:id:yazawa_tech:20190721170114p:plain

bundle exec rspec の実行結果を見てみると、ローカルホストで rspec コマンドを実行した時のようなメッセージが出力されている。

$ #!/bin/bash -eo pipefail
bundle exec rspec
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:46559
.....

Finished in 4.19 seconds (files took 1.52 seconds to load)
5 examples, 0 failures

これでリモートリポジトリにプッシュしてRSpec を実行させることができた。

まとめ

  • CircleCI を使ってテストの実行を自動化してみた
  • CircleCI のドキュメントがかなり充実しているし日本語版もあるので、最初は全くわからなかったが丁寧に読めば実現できる
  • 次はブランチを指定してのジョブの実行や、オートデプロイも実現できるようにしたい

CircleCI ではじめてのビルドを成功させる

f:id:yazawa_tech:20190720171857p:plain

自動化や効率化が大好きではあるものの、CI/CD のためのツールを今までJenkins 以外使用したことがなかったため、有名な CircleCI を試してみた。簡単な導入までのハンズオンの手順をまとめておく。

CircleCI とは

CI(Continuous Integration) を支援するためのツールで、特定のイベントに基づいたテストの実行や、指定した環境へのオートデプロイなど、開発のサイクルに必要なタスクを一部自動化し、使い方によっては開発効率を大幅に上げてくれる。

f:id:yazawa_tech:20190713201641p:plain

実際に使ってみる

使用するに当たって事前にサインアップが必要で、GitHubBitbucket でログインする必要がある。今回はGitHub でログインしてリポジトリと連携させてみる。

f:id:yazawa_tech:20190713201928p:plain

初回にGitHub でログインするボタンを押すと、自身のGitHub アカウントとCircleCI の連携を許可するか確認する画面が出てくるので、「Authorize circleci」をクリックして認証する。

f:id:yazawa_tech:20190713202324p:plain

認証が完了すると、GItHub アカウントでのログインが完了し、GitHubリポジトリが表示されているCircleCI のダッシュボードに移動する。

f:id:yazawa_tech:20190709061303p:plain
ログイン後の画面

ここでCircleCI で管理したいリポジトリ(= CI を導入したいリポジトリ)を選択して「Follow」ボタンをクリックする。

f:id:yazawa_tech:20190709061717p:plain

すると最初のビルドが実行されるが、Job の実行に失敗している。

f:id:yazawa_tech:20190709061827p:plain

これはdevelop ブランチの最新のコミットに対してビルドが実行されているが、恐らくGitHub の対象のリポジトリのデフォルトに設定されているブランチを対象にしていると思われる。

f:id:yazawa_tech:20190713203657p:plain
develop ブランチに「Default」が設定されている

上記のエラー画面の「#1」のリンクをクリックすると、ビルド実行時のログを見ることができる。

f:id:yazawa_tech:20190709061941p:plain

ここでは「CircleCI の設定が不足しているため」という意味合いのエラーが出ているため、ログに表示されているドキュメントページを参照しながら設定ファイルを書き換えていく。

hello-build

今回はもっとも簡単な例として、hello-build というOrb を使ってビルド時にHello yazawatech という文字列を出力させてみる。手順は以下のように実施する。

  1. Git 管理されているプロジェクトのルートディレクトリに .circleci/config.yml ファイルを作成
  2. hello-build orb をインポートする記述と共に設定ファイルを記述
  3. Git のリモートリポジトリへプッシュ

1. Git 管理されているプロジェクトのルートディレクトリに .circleci/config.yml ファイルを作成

$ mkdir .circleci
$ touch .circleci/config.yml

2. hello-build orb をインポートする記述と共に設定ファイルを記述

Orb とは「CircleCI を手早く使い始めるためのコンフィグパッケージ」のことで、 CircleCI Orbs レジストリでさまざまなOrb が公開されている。

circleci.com

今回はhello-build を使用するため、以下のように記述する。

version: 2.1

orbs:
    hello: circleci/hello-build@0.0.7 # uses the circleci/buildpack-deps Docker image

workflows:
    "Hello Workflow":
        jobs:
          - hello/hello-build

この状態でコミットとプッシュをすると、ビルドが実行されて、hello-world が出力される。

$ git add .circleci/config.yml
$ git commit -m 'Add circle ci config file'
$ git push

プロジェクトのダッシュボードを見てみる。

f:id:yazawa_tech:20190709064207p:plain
ビルドが成功している

f:id:yazawa_tech:20190709064412p:plain

無事にビルドに成功し、 Hello yazawatech という出力がされていることがわかる。

まとめ

  • 今回はCI ツールであるCircleCI を初めて触ってみた
  • CI ツールでは「特定のブランチにプッシュしたらデプロイする」といったイベントごとの制御ができるので、色々試したい