はじめに
ゴールデンウィークの学習として、Rubyの学習用リポジトリで、FizzBuzzプログラムを題材にして、テスト・静的解析・カバレッジ計測・CIの自動実行まで一通り設定しました。
題材は以下の書籍です。
今回やったことは以下です。
- FizzBuzzプログラムの作成
- RSpecの導入
- RSpecの設定
- RuboCopの導入
- RuboCopの設定
- SimpleCov / Codecovの導入
- Codecovの設定
- GitHub Actionsの導入
- GitHub Actionsの設定
- ブランチ保護ルールの設定
- Github Copilot,Codexのレビュー自動化
単にFizzBuzzを書くのではなく、実務でも使われるような開発フローに近づけるために、Pull Request作成時にテストと静的解析が自動で実行される構成を目指しました。
作成したもの
今回作成したのは、Rubyで実装したFizzBuzzプログラムです。
FizzBuzz自体は小さな題材ですが、以下のような開発環境まわりの練習を追加しました。
- RSpecでテストを書く
- RuboCopでコードスタイルをチェックする
- SimpleCovでテストカバレッジを計測する
- Codecovでカバレッジ結果を可視化する
- GitHub ActionsでCIを実行する
- ブランチ保護ルールでmainブランチへの直接反映を制限する
小さなプログラムでも、CIまで含めて構築すると、開発フロー全体の理解につながると感じました。
RSpecの導入
まず、RubyのテストフレームワークとしてRSpecを導入しました。
Gemfile に以下のようにRSpecを追加しました。
group :development, :test do
gem 'rspec', require: false
end
RSpecを導入することで、FizzBuzzの振る舞いを自動テストとして確認できるようになります。
RSpecの設定
RSpecでは、.rspec や spec_helper.rb を使って設定を行いました。
RSpecの設定では、主に以下のような内容を指定しました。
RSpec.configure do |config|
config.example_status_persistence_file_path = ".rspec_status"
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
それぞれの意味は以下です。
example_status_persistence_file_path は、前回失敗したテストなどの状態を保存するための設定です。
disable_monkey_patching! は、RSpecがグローバルにメソッドを追加する挙動を無効化する設定です。これにより、RSpecの機能を明示的に使う形になり、コードの見通しがよくなります。
expect_with :rspec と c.syntax = :expect は、RSpecの期待値構文として expect(...).to ... を使う設定です。
RuboCopの導入
次に、Rubyの静的解析ツールであるRuboCopを導入しました。
Gemfile には以下のように追加しました。
group :development, :test do
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rspec', require: false
end
RuboCopを導入することで、コードスタイルや書き方の問題を自動で検出できます。
今回は通常のRuboCopに加えて、以下も導入しました。
rubocop-performancerubocop-rspec
rubocop-performance は、パフォーマンス面で改善できるRubyコードを検出するための拡張です。
rubocop-rspec は、RSpecの書き方に関するルールを追加するための拡張です。
RuboCopの設定
.rubocop.yml では、以下のようにプラグインを読み込んでいます。
plugins:
- rubocop-rspec
- rubocop-performance
AllCops:
NewCops: enable
RSpec/ContextWording:
Enabled: false
plugins では、rubocop-rspec と rubocop-performance を有効化しています。
AllCops: NewCops: enable は、新しく追加されたCopを有効化する設定です。
RSpec/ContextWording: Enabled: false は、RSpecの context の文言に関するルールを無効化する設定です。
RSpecでは context に「〜の場合」のような日本語を書きたかったため、このルールは今回の書き方に合わせて無効化しました。
Codecovの導入
テストカバレッジを可視化するためにCodecovを導入しました。
Ruby側では、SimpleCovとsimplecov-coberturaを使いました。
group :test do
gem 'simplecov', require: false
gem 'simplecov-cobertura', require: false
end
SimpleCovは、RSpec実行時にテストカバレッジを計測するためのgemです。
simplecov-coberturaは、Codecovに渡しやすいXML形式のカバレッジレポートを出力するために使いました。
RSpecを実行すると、coverage/coverage.xml が生成され、そのファイルをGitHub ActionsからCodecovへアップロードする構成にしました。
Codecovの設定
codecov.yml では、Codecovのコメントやステータスチェックの挙動を設定しました。
comment:
layout: "diff, flags, files"
behavior: default
require_changes: false
require_base: false
require_head: true
hide_project_coverage: false
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true
comment では、Pull Requestに表示されるCodecovコメントの内容を設定しています。
layout: "diff, flags, files" により、差分、flags、ファイルごとの情報を表示するようにしています。
coverage.status.project.default.informational: true と coverage.status.patch.default.informational: true は、カバレッジ結果を情報として扱う設定です。
つまり、カバレッジが下がった場合でも、CIを必ず失敗させるのではなく、まずは学習用として結果を確認できるようにしました。
GitHub Actionsの導入
GitHub Actionsでは、Pull Request作成時とmainブランチへのpush時にCIが実行されるようにしました。
ワークフロー名は以下です。
name: Ruby Book CI
実行タイミングは以下のように設定しました。
on:
pull_request:
branches:
- main
push:
branches:
- main
これにより、mainブランチ向けのPull Requestと、mainブランチへのpush時にCIが実行されます。
GitHub Actionsの設定
GitHub Actionsでは、以下の2つのjobを作成しました。
- RuboCop
- RSpec and Codecov
RuboCopのjobでは、リポジトリをcheckoutし、Rubyをセットアップしてから、RuboCopを実行します。
jobs:
rubocop:
name: RuboCop
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0.1"
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop
RSpecとCodecovのjobでは、RSpecを実行したあと、Codecovへカバレッジ結果をアップロードします。
rspec:
name: Rspec and Codecov
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0.1"
bundler-cache: true
- name: Run RSpec
run: bundle exec rspec
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: coverage/coverage.xml
flags: ruby
name: ruby-rspec
fail-ci-if-error: true
bundler-cache: true を指定することで、bundle installの結果をキャッシュし、CIの実行時間を短縮できます。
また、Codecovのアップロードには CODECOV_TOKEN を使うため、GitHubのRepository secretsにトークンを設定しました。
ブランチ保護ルールの設定
最後に、mainブランチを保護するために、GitHubのブランチ保護ルールを設定しました。
目的は、mainブランチに対して直接変更が入るのを防ぎ、Pull Request経由で変更する運用にすることです。
設定した内容は、主に以下です。
- mainブランチを保護対象にする
- Pull Request経由でのマージを必須にする
- CIが成功していない場合はマージできないようにする
- 必要に応じてレビューを必須にする
これにより、RSpecやRuboCopが失敗しているコードをmainブランチへ取り込むことを防げます。
個人開発の学習リポジトリでも、ブランチ保護ルールまで設定すると、実務に近い開発フローを体験できます。
詰まったところ・学んだこと
今回の設定で特に学びがあったのは、以下の点です。
RSpecでは標準入力・標準出力のテストに工夫が必要
FizzBuzzのように標準入力を受け取るプログラムでは、RSpecで $stdin を差し替えてテストする必要がありました。
また、標準出力を検証する場合は、expect { ... }.to output(...).to_stdout のように、ブロック形式で書く必要があります。
最初は expect(fizz_buzz).to output(...) のように書いてしまい、RSpecのエラーからブロック形式で渡す必要があることを理解しました。
RuboCopはデフォルトだけでなく、プロジェクトに合わせた調整も必要
RuboCopはデフォルトでも便利ですが、RSpecの context に日本語を使う場合など、自分のプロジェクトに合わせて一部ルールを調整する必要がありました。
今回でいうと、RSpec/ContextWording を無効化したのがその例です。
Codecovにアップロードするには、カバレッジファイルの形式が重要
Codecovに結果を送るには、RSpecを実行するだけではなく、Codecovが読み取れるカバレッジファイルを生成する必要がありました。
そのために、SimpleCovに加えてsimplecov-coberturaを導入し、coverage/coverage.xml を生成する構成にしました。
GitHub ActionsはCIの入口で、ブランチ保護ルールと組み合わせて意味が強くなる
GitHub ActionsのYAMLを追加するだけでもCIは実行されます。
ただし、mainブランチを守るという意味では、ブランチ保護ルールと組み合わせることが重要だと感じました。
CIが失敗していてもマージできてしまう設定だと、品質ゲートとしては弱くなります。
そのため、GitHub Actionsの設定とブランチ保護ルールはセットで考える必要があると学びました。
まとめ
今回は、RubyのFizzBuzzプログラムを題材にして、以下を導入しました。
- RSpecによる自動テスト
- RuboCopによる静的解析
- SimpleCov / Codecovによるカバレッジ計測
- GitHub ActionsによるCI
- ブランチ保護ルールによるmainブランチ保護
FizzBuzz自体は小さなプログラムですが、テスト・静的解析・カバレッジ・CI・ブランチ保護まで含めることで、実務に近い開発フローを学ぶことができました。
今後は、以下のような改善も試していきたいです。
- READMEにCIバッジとCodecovバッジを追加する
- RSpecのテストケースを増やす
- GitHub Actionsのjob名をブランチ保護ルールの必須チェックに設定する
- Pull Requestテンプレートを追加する
- Dependabotを導入してgemやGitHub Actionsの更新を自動化する