はじめに
はじめまして、@h-sakanoと申します。
この記事では、以下の内容について取り扱います。
- RailsのテストフレームワークRSpecを並列で走らせる方法
- 並列で走らせたテストのカバレッジを出力する方法
取り扱わない内容
- RSpecの詳細説明・設定方法
- テストフレームワーク
- SimpleCovの詳細説明・設定方法
- コードカバレッジの計算用gem
環境
- Ruby on Rails 5.2.2
- CircleCI 2.1
一部CircleCIのバージョン2.1で使えるようになった新機能を使いますので、PROJECT SETTINGS
-> Advance Settings
-> Enable build processing
で有効化しておきます。
使用gem
テストに使用するGemを抜き出したものが以下になります。
group :test do
# Provides Rails integration for factory_bot
gem "factory_bot_rails"
# Generates data at random
gem "faker"
# Rails: 2 CPUs = 2x Testing Speed for RSpec, Test::Unit and Cucumber
gem "parallel_tests"
# Testing framework for Rails 3.x, 4.x and 5.x
gem "rspec_junit_formatter"
gem "rspec-rails"
# Code coverage for Ruby
gem "simplecov", require: false
end
今回はテストフレームワークとしてRSpecを使用するので、rspec-rails
とそれに関連するgemをインストールします。
また、RSpecを並列実行するため、parallel_tests
をインストールします。
parallel_testsの設定
parallel_tests
をインストールすると、bundle exec parallel_rspec
コマンドでRSpecの並列実行が可能になります。
また、parallel_rspec
コマンド実行時の引数は、プロジェクトのルートディレクトリに.rspec_parallel
ファイルを作成することで設定することができます。
--require spec_helper
--format progress
--format RspecJunitFormatter
--out rspec/rspec<%= ENV['TEST_ENV_NUMBER'] %>.xml
--profile 10
--out
オプションで出力ファイル名にTEST_ENV_NUMBER
という環境変数を使用しています。
この変数には並列実行した際のプロセス番号が自動で割り振られます。
Process number | 1 | 2 | 3 |
---|---|---|---|
ENV['TEST_ENV_NUMBER'] | '' | '2' | '3' |
この変数を使用することで、並列実行時のテスト結果をそれぞれ分けて保存することができます。
database.yml
の設定もこのTEST_ENV_NUMBER
を使って書き換えます。
test:
<<: *default
database: db/test<%= ENV['TEST_ENV_NUMBER'] %>.sqlite3
今回はデータベースにSQLite 3を使用していますが、MySQLやPostgreSQLを使用する場合も、database
の値にTEST_ENV_NUMBER
を入れれば大丈夫です。
parallel_testsの実行
まずは手元でparallel_testsが実行できることを確認します。
例えば、4つのプロセスで並列実行する場合を考えます。
テスト用データベースのセットアップを行います。
例えば、以下のコマンドで4つのテスト用データベースが作成されます。
$ bundle exec rails parallel:setup[4]
Created database 'db/test3.sqlite3'
Created database 'db/test2.sqlite3'
Created database 'db/test4.sqlite3'
Created database 'db/test.sqlite3'
先程設定したdatabase.yml
のデータベース名が反映されます。
テスト用データベースのセットアップが終わったら、次はRSpecを並列実行します。
bundle exec parallel_rspec -n 4 spec
CircleCIの設定
以上を踏まえて、CircleCIでこれらのコマンドを実行するよう、設定ファイルを作成します。
最終的な設定ファイルはgistにアップロードしています。
この設定ではrspec
ジョブとcoverage
ジョブの2つのジョブを定義しています。
rspec
ジョブでRSpec
を並列実行し、coverage
ジョブではSimpleCov
を使用してテストカバレッジを出力します。
workflows:
build:
jobs:
- rspec
- coverage:
requires:
- rspec
このように、まずはrspec
ジョブを実行し、その後にcoverage
ジョブを実行するようにしています。
それでは、ファイルの頭から解説していきます。
version: 2.1
executors:
default:
working_directory: ~/repo
docker:
- image: circleci/ruby:2.5.1-node-browsers
environment:
RACK_ENV: test
RAILS_ENV: test
commands:
setup_packages:
steps:
- restore_cache:
keys:
- v1-dependencies-{{ checksum "Gemfile.lock" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: install dependencies
command: |
bundle install --jobs=4 --retry=3 --path vendor/bundle
- save_cache:
paths:
- ./vendor/bundle
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
version 2.1からexecutors
, commands
, parameters
という機能が使用できます。
executors
では実行環境設定を複数ジョブで使い回すための機能です。
今回は、rpec
ジョブとcoverage
ジョブで同じ実行環境を使用するため、executors
でdefault
という名前の実行環境を定義して、使い回せるようにしています。
commands
は複数ジョブで一連のコマンドを使い回すための機能です。
rspec
ジョブでもcoverage
ジョブでも、必要なGemのインストール等の処理は必要になるため、commands
にてsetup_packages
という名前で一連の処理をまとめて定義しています。
なお、perameters
は残念ながら今回は使用していません。
rspec:
executor:
name: default
rspec
ジョブの実行環境は前もってexecutors
で定義しておいたdefault
を使います。
environment:
PARALLEL_TESTS_CONCURRENCY: 4
parallelism: 2
ここでは、並列実行数を環境変数で設定しています。
また、今回はCircleCIを2つのコンテナで使用することにしましたので、parallelism
に2をセットしています。
(複数コンテナでのテスト実行は有料となります)
steps:
- checkout
- setup_packages
# Database setup
- run: bundle exec rails parallel:setup[${PARALLEL_TESTS_CONCURRENCY}]
# run tests!
- run:
name: run tests
command: |
mkdir /tmp/test-results
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
bundle exec parallel_rspec -n ${PARALLEL_TESTS_CONCURRENCY} $TEST_FILES
- run:
name: Stash coverage results
command: |
mkdir coverage_results
cp -R coverage/.resultset.json coverage_results/.resultset-${CIRCLE_NODE_INDEX}.json
- persist_to_workspace:
root: .
paths:
- coverage_results
# collect reports
- store_test_results:
path: /tmp/test-results
- store_artifacts:
path: /tmp/test-results
destination: test-results
rspec
ジョブでデータベースを準備してparallel_rspec
を実行して、テスト結果を保存します。
複数コンテナでRSpec
を並列実行している場合、SimpleCovはそれぞれのコンテナごとに別個に結果を出力してしまうので、ここでは、出力ファイルを次のジョブで使用できるようにpersist_to_workspace
を実行するだけにとどめておきます。
coverage:
executor:
name: default
steps:
- checkout
- setup_packages
- attach_workspace:
at: .
- run: bundle exec rails simplecov:report_coverage
- store_artifacts:
path: ~/repo/coverage
destination: coverage
attach_workspace
でrspec
ジョブでpersist_to_workspace
しておいた、coverage_results
をカレントディレクトリにアタッチします。
coverage
ジョブではrspec
ジョブで出力されたSimpleCov
の結果をbundle exec rails simplecov:report_coverage
でマージして、CircleCIのテスト結果から見れるようにstore_artifacts
を実行しています。
ここで実行しているsimplecov:report_coverage
タスクは自前で作ったタスクです。
coverage_results
ディレクトリに保存されたSimpleCov
の出力をマージします。
# frozen_string_literal: true
require "active_support/inflector"
require "simplecov"
class SimpleCovHelper
def self.report_coverage(base_dir: "./coverage_results")
SimpleCov.start "rails" do
merge_timeout(3600)
end
new(base_dir: base_dir).merge_results
end
attr_reader :base_dir
def initialize(base_dir:)
@base_dir = base_dir
end
def all_results
Dir["#{base_dir}/.resultset*.json"]
end
def merge_results
results = all_results.reduce([]) { |result, file|
JSON.parse(File.read(file)).each { |key, value|
result << SimpleCov::Result.from_hash("#{key}": value)
}
result
}
SimpleCov::ResultMerger.merge_results(*results).tap do |result|
SimpleCov::ResultMerger.store_result(result)
end
end
end
# frozen_string_literal: true
require_relative "../../spec/simplecov_helper"
namespace :simplecov do
desc "merge results"
task report_coverage: :environment do
SimpleCovHelper.report_coverage
end
end
まとめ
もともと開発中のプロジェクトでは、CircleCI2.0で上記の設定を行っていたのですが、今回の記事を書くにあたって、CircleCI2.1で実装された便利な機能を使って書き直してみました。
まだ、ちゃんとキャッチアップできていないのですがOrbs
を使って設定を共有できるみたいなので、それも試してみたいと思っています。