Docker で Bundler を使って Ruby の gem を作成する方法

<!-- How to create a Ruby gem with Bundler on Docker -->

プログラミングのツールやサービスは便利なものが揃っているので Ruby の gem を作りながら紹介します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/38f3ac6b-9da6-6b89-b6a0-9297b52dcae9.png)

- Docker なので OS X, Windows, Linux で動作します。
- gem は Ruby の外部ライブラリです。

# すること、できること

- [Docker](https://www.docker.com/) の [Ruby](https://www.ruby-lang.org/) で gem をテスト駆動開発で作ります。
- [CircleCI](https://circleci.com/) や [CodeClimate](https://codeclimate.com/) のサービスで自動テストをします。
- [GitHub](https://github.com/) や [RubyGems](https://rubygems.org/) にリリースした gem を使います。

# GitHub の利用

- [GitHub](https://github.com/) はソースコードを管理するサービスです。
- 今回は _my_gems_ という名前の gem を作るので _my_gems_ のリポジトリを作ります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/48eb3425-9e20-cf23-523e-87d4dfcc04c3.png)

- リポジトリを作ると続きはターミナルでコマンドラインを使います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/597d9c06-d1ed-d49e-44e6-b85f2df346e5.png)

- `git init` コマンドで空の README を追加をして、`git config` コマンドでユーザ情報を設定します。
- `git commit` コマンドでコミットができたら、`git log` コマンドでログを確認しましょう。

```bash
$ mkdir my_gems
$ cd my_gems/
$ git init && touch README.md && git add README.md
$ git config --local user.name "your-name"
$ git config --local user.email "your-name@example.com"
$ git commit -m "Initial commit" && rm README.md
$ git log --pretty=format:"%an <%ae> : %s"
your-name <your-name@example.com> : Initial commit
```

- `git remote add` コマンドで GitHub の URL を設定します。
- `git push` コマンドで GitHub にソースコードがアップロードされます。

```bash
$ git remote add origin https://github.com/your-name/my_gems.git
$ git push -u origin master
```

- README だけのリポジトリができました！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/a3e5e2d9-7551-4b7b-de2a-10690d8e45bc.png)

- `.netrc` ファイルに`git push` コマンドで要求されるユーザ情報を登録することができます。
  - ユーザ情報は暗号化することもできます。

```yaml
machine github.com
login your-name@example.com
password your-password
```

# Docker と Bundler で gem の作成

- [Docker](https://www.docker.com/) はコンテナ型の仮想環境を操作するツールです。
- [Bundler](https://bundler.io/) は gem の依存を管理したり gem の作成をサポートするツールです。
- `docker run -v $(pwd):/my_gems ruby:2.5.1` コマンドは Ruby が使える仮想環境でコマンドを実行します。
  - `-v $(pwd):/my_gems` のオプションで仮想環境のボリューム（フォルダ）をホストとマウントします。
    - `$(pwd)` は絶対パスが設定されます。任意で変更してください。
- `bundle gem my_gems --test=rspec --mit --no-coc` コマンドで gem の雛形を作ります。
  - `--test=rspec` のオプションで Test Framework の RSpec を利用します。
  - `--mit --no-coc` のオプションで license は MIT、Contributor Code of Conduct の出力は不要にします。

```bash
$ docker run -v $(pwd):/my_gems ruby:2.5.1 bundle gem my_gems --test=rspec --mit --no-coc
Creating gem 'my_gems'...
MIT License enabled in config
      create  my_gems.Gemfile
      create  my_gems.lib.my_gems.rb
      create  my_gems.lib.my_gems.version.rb
      create  my_gems.my_gems.gemspec
      create  my_gems.Rakefile
      create  my_gems.README.md
      create  my_gems.bin.console
      create  my_gems.bin.setup
      create  my_gems..gitignore
      create  my_gems..travis.yml
      create  my_gems..rspec
      create  my_gems.spec.spec_helper.rb
      create  my_gems.spec.my_gems_spec.rb
      create  my_gems.LICENSE.txt
Initializing git repo in /my_gems
Gem 'my_gems' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html
```

- `my_gems.gemspec` ファイルの TODO の箇所を修正します。

```diff
-  spec.authors       = ["TODO: Write your name"]
-  spec.email         = ["TODO: Write your email address"]
+  spec.authors       = ["your-name"]
+  spec.email         = ["your-name@example.com"]

-  spec.summary       = %q{TODO: Write a short summary, because RubyGems requires one.}
-  spec.description   = %q{TODO: Write a longer description or delete this line.}
-  spec.homepage      = "TODO: Put your gem's website or public repo URL here."
+  spec.summary       = %q{my gems of short summary.}
+  spec.description   = %q{my gems of longer description.}
+  spec.homepage      = "https://github.com/your-name/my_gems.git"
```

- `Gemfile` ファイルに開発で使う gem を追加します。

```ruby
gem 'guard-rspec', require: false, group: %i[development test]
gem 'rspec', group: :test
gem 'rspec_junit_formatter', group: :test
gem 'rubocop', require: false, group: %i[development test]
gem 'simplecov', require: false, group: :test
```

- `git commit` コマンドでコミットします。

```bash
$ git add --all && git commit -m "Bundler New gem"
```

# Docker Compose で Docker の構築

- [Docker Compose](https://docs.docker.com/compose/) は複数の Docker を管理するツールです。
- `touch` コマンドで空ファイルを作成します。

```bash
$ touch Dockerfile && touch docker-compose.yml
```

- `Dockerfile` ファイルに仮想環境で使う Ruby のバージョンを設定します。
  - `locale-gen` で日本語の対応をしています。

```Dockerfile
FROM ruby:2.5.1
RUN apt-get update -qq && apt-get install -y locales
RUN sed -i 's/#.*ja_JP\.UTF/ja_JP\.UTF/' /etc/locale.gen
RUN locale-gen && update-locale LANG=ja_JP.UTF-8
RUN mkdir /my_app
WORKDIR /my_app
COPY . /my_app
RUN bundle install
```

- `docker-compose.yml` ファイルに仮想環境のボリューム（フォルダ）とホストのマウントを設定します。

```yaml
version: "3"
services:
  app:
    build: .
    volumes:
      - .:/my_app
    environment:
      - LANG=ja_JP.UTF-8
      - LC_CTYPE=ja_JP.UTF-8
```

- `docker-compose build` コマンドで仮想環境を構築すると `rake -T` コマンドでタスクを確認できます。

```bash
$ docker-compose build
$ docker-compose run app rake -T
rake build            # Build my_gems-0.1.0.gem into the pkg directory
rake clean            # Remove any temporary products
rake clobber          # Remove any generated files
rake install          # Build and install my_gems-0.1.0.gem into system gems
rake install:local    # Build and install my_gems-0.1.0.gem into system gems without network access
rake release[remote]  # Create tag v0.1.0 and build and push my_gems-0.1.0.gem to TODO: Set to 'http://mygemserver.com'
rake spec             # Run RSpec code examples
```

- `git commit` コマンドでコミットします。

```
$ git add --all && git commit -m "Docker Compose build"
```

# CircleCI の利用

- [CircleCI](https://circleci.com/) は自動でテストやデプロイを実行するサービスです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/ec3a44db-91a6-fef8-74ed-50a75218bcb2.png)

- `touch` コマンドで空ファイルを作成します。

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

- `.circleci/config.yml` ファイルにテストの実行とレポートの送信を設定します。

```yaml
version: 2
jobs:
  build:
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: Install Dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle
      - run:
          name: Setup Code Climate test-reporter
          command: |
            curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
            chmod +x ./cc-test-reporter
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
      - run:
          name: Run Tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
            ./cc-test-reporter before-build
            bundle exec rspec --format progress \
                            --format RspecJunitFormatter \
                            --out /tmp/test-results/rspec.xml \
                            --format progress \
                            $TEST_FILES
            ./cc-test-reporter after-build --coverage-input-type simplecov --exit-code $?
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results
```

- `circleci build` コマンドでテストを実行することができます。
  - Bundler で作成した RSpec の雛形はテストが失敗します。

```bash
$ circleci build
Error: runner failed
Task failed
```

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```bash
$ git add --all && git commit -m "CircleCI config"
$ git push origin master
```

- CircleCI の `Add Projects` から _my_gems_ で `Set Up Project` をクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/46dba500-aa01-81c8-a1a7-7e6c793b3750.png)

- CircleCI で `Start building` をクリックするとテストが実行されます。
  - 今回はテストに失敗したことがレポートされています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/2c692371-8465-4a82-c37a-db18af5bb0f2.png)

# Code Climate の利用

- [Code Climate](https://codeclimate.com/) は自動でコードの品質をチェックするサービスです。
- Code Climate で `Add a repository` をクリックして _my_gems_ を追加します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/6fb58b16-5052-7f43-07cd-164145bec2da.png)

- カバレッジは未対応なので傘のアイコンが表示されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/f2b254cb-a9f7-2e4e-e014-a71df78cb403.png)

- `touch` コマンドで空ファイルを作成します。

```bash
$ touch .codeclimate.yml
```

- `.codeclimate.yml` ファイルに Rubocop の利用を設定します。

```yaml
version: "2"
plugins:
  rubocop:
    enabled: true
```

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```
$ git add --all && git commit -m "Code Climate config"
$ git push origin master
```

- Code Climate で品質に問題があることを指摘されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/7b1b13e9-91b2-f1ac-0cc8-5d2477415c4c.png)

# Rubocop の利用

- [Rubocop](https://github.com/rubocop-hq/rubocop) はコーディング規約のツールです。
- `touch` コマンドで空ファイルを作成します。

```bash
$ touch .rubocop.yml
```

- `.rubocop.yml` ファイルに Rubocop のコーディング規約を設定します。

```yaml
AllCops:
  Exclude:
    - "bin/*"
    - "spec/spec_helper.rb"
    - "Guardfile"
    - "Gemfile"
    - "*.gemspec"
    - "Rakefile"

Style/Documentation:
  Enabled: false

Metrics/BlockLength:
  Exclude:
    - "spec/**/*"
```

- `bundle exec rubocop` コマンドで規約違反を確認することができます。

```bash
$ docker-compose run app bundle exec rubocop
Inspecting 3 files
CCC
```

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```
$ git add --all && git commit -m "Rubocop config"
$ git push origin master
```

- Code Climate も同じ結果になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/c352c326-92d6-b1bb-0aab-d34658561878.png)

# SimpleCov の利用

- [SimpleCov](https://github.com/colszowka/simplecov) はテストのカバレッジをサポートするツールです。
- Code Climate の `Test coverage` で `TEST REPORTER ID` の値を CircleCI に設定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/1dcc557f-8fe9-33b6-2fea-0928cb2447f6.png)

- CircleCI で Environment の `Add Variable` をクリックして `CC_TEST_REPORTER_ID` を追加します。
- Code Climate Enterprise は `CC_TEST_REPORTER_COVERAGE_ENDPOINT` も追加します。
  - https://[your-domain]/api/v1/test_reports

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/969d73f0-e88b-e844-9963-44f794924abb.png)

- `spec/spec_helper.rb` ファイルに SimpleCov の設定をします。

```ruby
require "bundler/setup"
require "simplecov"
SimpleCov.start do
  add_filter "/spec/"
end
require "my_gems"

RSpec.configure do |config|
  # Enable flags like --only-failures and --next-failure
  config.example_status_persistence_file_path = ".rspec_status"

  # Disable RSpec exposing methods globally on `Module` and `main`
  config.disable_monkey_patching!

  config.expect_with :rspec do |c|
    c.syntax = :expect
  end
end
```

- `bundle exec rake spec` コマンドでテストを実行します。
- `coverage/index.html` ファイルにカバレッジが出力されます。

```bash
$ docker-compose run app bundle exec rake spec
$ open coverage/index.html
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/f0afff1d-c414-9b2a-2431-eed514bbba73.png)

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```
$ git commit -am "SimpleCov config"
```

# テスト駆動開発

- [テスト駆動開発](https://ja.wikipedia.org/wiki/TDD) はプログラミングの開発手法で、最初にテストから記述（テストファースト）します。
- テスト駆動開発は Red Green Refactor のサイクルを繰り返します。
  - まず、テストから書いて `Red` (Assert First)
  - すぐ、テストを成功させて `Green` (Fake It)
  - テストが成功の状態で `Refactor` (Refactoring)

# Guard RSpec の利用

- [Guard](https://github.com/guard/guard) はテストの実行をサポートするツールです。
- `bundle exec guard init` コマンドで Guardfile を出力します。
- `bundle exec guard` コマンドで自動テストの準備をします。

```bash
$ docker-compose run app bundle exec guard init
$ docker-compose run app bundle exec guard
[1] guard(main)>
```

- [FizzBuzz](https://ja.wikipedia.org/wiki/Fizz_Buzz) のプログラムを作ります。
- `spec/my_gems_spec.rb` ファイルに FizzBuzz クラスの仕様を書きます。

```ruby
require 'spec_helper'

RSpec.describe MyGems do
  describe MyGems::FizzBuzz do
    context '３で割り切れる場合' do
      example 'Fizz という'
    end

    context '５で割り切れる場合' do
      example 'Buzz という'
    end

    context '１５で割り切れる場合' do
      example 'FizzBuzz という'
    end

    context '３や１５で割り切れない場合' do
      example 'その 数字 をいう'
    end
  end
end
```

- `lib/my_gems.rb` ファイルに FizzBuzz のクラスを仮実装します。

```ruby
require 'my_gems/version'

module MyGems
  class FizzBuzz
    def initialize(number)
      @number = number
    end

    def say
      ''
    end
  end
end
```

- ファイルが保存されると自動でテストが実行されます。

```bash
MyGems
  MyGems::FizzBuzz
    ３で割り切れる場合
      Fizz という (PENDING: Not yet implemented)
    ５で割り切れる場合
      Buzz という (PENDING: Not yet implemented)
    １５で割り切れる場合
      FizzBuzz という (PENDING: Not yet implemented)
    ３や１５で割り切れない場合
      その 数字 をいう (PENDING: Not yet implemented)
```

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```
$ git add --all && git commit -m "Guard config"
$ git push origin master
```

- CircleCI は全てペンディングなので成功が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/87376fe5-945d-deb4-38df-dad758a7a755.png)

- Code Climate はカバレッジが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/d40294ff-4425-92e1-5244-2110f8c56dac.png)

# Pull request

- [Pull request](https://help.github.com/articles/about-pull-requests/) は [GitHub Flow ](https://help.github.com/articles/github-flow/)という開発フローのプロセスの一つです。
- `git checkout` コマンドで開発のブランチを作成します。
- `bundle exec guard` コマンドで Guard RSpec を実行します。

```bash
$ git checkout -b develop
$ docker-compose run app bundle exec guard
```

- `spec/my_gems_spec.rb` ファイルに say メソッドの仕様を記述します。

```ruby
require 'spec_helper'

RSpec.describe MyGems do
  describe MyGems::FizzBuzz do
    describe '#say' do
      let(:fizz_buzz) { described_class.new(number) }
      subject { fizz_buzz.say }

      context '３で割り切れる場合' do
        let(:number) { 3 }
        example 'Fizz という' do
          expect(subject).to eq 'Fizz'
        end
      end

      context '５で割り切れる場合' do
        let(:number) { 5 }
        example 'Buzz という' do
          expect(subject).to eq 'Buzz'
        end
      end

      context '１５で割り切れる場合' do
        let(:number) { 15 }
        example 'FizzBuzz という' do
          expect(subject).to eq 'FizzBuzz'
        end
      end

      context '３や１５で割り切れない場合' do
        let(:number) { 1 }
        example 'その 数字 をいう' do
          expect(subject).to eq '1'
        end
      end
    end
  end
end
```

- say メソッドは `Fizz` の戻り値を期待しているが、違う値なのでエラーになります。

```bash
Failures:

  1) MyGems MyGems::FizzBuzz#say ３で割り切れる場合 Fizz という
     Failure/Error: expect(subject).to eq 'Fizz'

       expected: "Fizz"
            got: ""
```

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```
$ git commit -am "Test example"
$ git push origin develop
```

- GitHub に Pull request を登録すると CircleCI と Code Climate の状態が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/428b057d-de46-9aea-fe31-c370864fa3bf.png)

- CircleCI は失敗が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/33b16a30-c732-cc96-8e6d-ace880254c85.png)

- `lib/my_gems.rb` ファイルに何かテストが成功するように実装します。(Fake It)

```ruby
require 'my_gems/version'

module MyGems
  class FizzBuzz
    def initialize(number)
      @number = number
    end

    def say
      'Fizz'
    end
  end
end
```

- `lib/my_gems.rb` ファイルにいくつかのテストが成功するように実装します。(Refactoring)

```ruby
require 'my_gems/version'

module MyGems
  class FizzBuzz
    def initialize(number)
      @number = number
    end

    def say
      @number == 3 ? 'Fizz' : 'Buzz'
    end
  end
end
```

- `lib/my_gems.rb` ファイルにリファクタリングを繰り返して、全てのテストが成功するように実装します。

```ruby
require 'my_gems/version'

module MyGems
  class FizzBuzz
    def initialize(number)
      @number = number
    end

    def say
      return 'FizzBuzz' if (@number % 15).zero?
      return 'Buzz' if (@number % 5).zero?
      return 'Fizz' if (@number % 3).zero?

      @number.to_s
    end
  end
end
```

- `git commit` コマンドでコミットして、`git push` コマンドでアップロードします。

```
$ git commit -am "Pass a test"
$ git push origin develop
```

- GitHub の Pull request は自動で CircleCI と Code Climate の状態が更新されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/08edc923-fe5e-cb27-2d32-c28356ce5351.png)

- CircleCI がテストを成功しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/7a3a08f4-3400-1e99-b35b-7d5db4e6b0b5.png)

- Github で `Merge pull request` をクリックすると Code Climate のカバレッジが更新されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/83c4574d-ca45-361b-343d-e8fff6202d52.png)

- README に CircleCI と Code Climate の状態をバッジを表示することができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/4bc6aa51-0fb5-637d-a6fe-3852249c0b18.png)

# GitHub の gem を利用

- GitHub にアップロードされた gem は別のプログラムから読み込むことができます。
- `touch` コマンドで空ファイルを作成します。

```bash
$ mkdir my_app
$ cd my_app/
$ touch app.rb && touch Gemfile && touch Gemfile.lock
$ touch Dockerfile && touch docker-compose.yml
```

- `app.rb` ファイルに FizzBuzz を使うコードを記述します。

```ruby
require 'my_gems'

fizz_buzz = MyGems::FizzBuzz.new(3)
puts fizz_buzz.say
```

- `Gemfile` ファイルに GitHub のパスを設定します。

```ruby
gem 'my_gems', git: "https://github.com/your-name/my_gems.git", branch: :master
```

- `Dockerfile` ファイルに仮想環境で使う Ruby のバージョンを設定します。
  - `Gemfile` ファイルを仮想環境にコピーします。

```Dockerfile
FROM ruby:2.5.1
RUN mkdir /my_app
WORKDIR /my_app
COPY Gemfile /my_app/Gemfile
COPY Gemfile.lock /my_app/Gemfile.lock
RUN bundle install
COPY . /my_app
```

- `docker-compose.yml` ファイルに仮想環境のボリューム（フォルダ）とホストのマウントを設定します。

```yaml
version: "3"
services:
  app:
    build: .
    volumes:
      - .:/my_app
```

- `bundle exec ruby app.rb` コマンドで Fizz が出力されます。

```bash
$ docker-compose run app bundle exec ruby app.rb
Fizz
```

- `Gemfile.lock` ファイルの `remote` は GitHub を参照しています。

```
GIT
  remote: https://github.com/your-name/my_gems.git
  revision: aa1f28a6c59e09892340d32d8cad76bc5b2154a4
  branch: master
  specs:
    my_gems (0.1.0)
```

# RubyGems の gem を利用

- [RubyGems](https://rubygems.org/) にアップロードされた gem は別のプログラムから読み込むことができます。
- `Gemfile` ファイルに RubyGems のパスを設定します。

```ruby
source "https://rubygems.org"

gem 'my_gems'
```

- `my_gems.gemspec` ファイルの allowed_push_host に URL を設定します。

```ruby
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
```

- `rake release` コマンドで gem を RubyGems にアップロードします。

```bash
$ rake release
my_gems 0.1.0 built to pkg/my_gems-0.1.0.gem.
Tagged v0.1.0.
Pushed git commits and tags.
Pushed my_gems 0.1.0 to rubygems.org
```

- `bundle exec ruby app.rb` コマンドで Fizz が出力されます。

```bash
$ docker-compose run app bundle
$ docker-compose build
$ docker-compose run app bundle exec ruby app.rb
Fizz
```

- `Gemfile.lock` ファイルの `remote` は RubyGems を参照しています。

```
GEM
  remote: https://rubygems.org/
  specs:
    my_gems (0.1.0)
```

- `lib/my_gems/version.rb` ファイルの `0.1.0` の値が gem のバージョンです。
  - gem の内容を変更してバージョンアップするにはこのファイルを修正します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/98c2d03e-296c-0342-c262-30b8f01d8a60.png)

# 完成したコード

- [GitHub](https://github.com/ogom/my_gems) に _my_gems_ のコードを公開しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/5e7ccdd4-a616-1452-767f-9c3474e144bc.png)

# 参考

- [プロを目指す人のための Ruby 入門](https://ruby-book.jnito.com/)
  - **FizzBuzz** プログラムの例題が掲載されています。