0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RSpecのCI待ちをCircleCI Smarter Testingで削る - TIAのテスト選択を手元で確かめる

0
Last updated at Posted at 2026-10-06

RSpecのテストは、モデルやリクエストのspecが増えるほど実行に時間がかかるようになります。数行の修正でも全specを実行する構成のままだと、プルリクエストを出すたびにCIの結果を長く待つことになります。

CircleCIのSmarter Testingを使うと、変更したファイルに関係するspecだけを選んで実行できます。この選択を担うのがTest Impact Analysisです。RSpecでは公式プラグインのrspec-circleci-coverageが、どのspecがどのファイルを実行したかを記録します。

この記事では、小さなRSpecプロジェクトを作りながら、Test Impact Analysisを手元で動かすまでの手順を確認します。あわせて、shared examplesやYAMLから生成するspecのように、カバレッジだけでは変更との関係を追えないspecがどう扱われるかと、その塞ぎ方も手元で再現します。

この記事でわかること

  • rspec-circleci-coverageと.circleci/test-suites.ymlの設定を、doctorで確かめながら仕上げる手順
  • ソースファイルを変えたときに、関係するspecだけが選ばれる様子
  • shared examplesとYAMLを使うspecが選ばれないケースと、test-selection-rulesでの塞ぎ方
  • CircleCIのジョブへの組み込み方

検証環境

対象 バージョン
Ruby 4.0.6
circleci CLI 1.0.51648(testsuiteサブコマンド 1.0.61831)
rspec-circleci-coverage 0.1.659

Test Impact AnalysisはSmarter Testingの有料機能(opt-in)です。料金はCircleCIの料金ページを確認してください。この記事のローカルでの確認(--local)では、impact dataは手元のファイルに保存されます。

サンプルプロジェクトを用意する

税込価格と割引率を計算する3つのクラスと、それを確かめる4つのspecを作ります。specの書き方をわざと変えてあり、あとの手順で「選ばれるspec」と「選ばれないspec」を見比べられます。

  • price_spec.rb: Priceを直接呼ぶ、ふつうのspec
  • price_loop_spec.rb: 配列のループでexampleを生成するspec
  • discount_data_spec.rb: YAMLファイルの行ごとにexampleを生成するspec
  • price_shared_spec.rb: ローカルのitと、spec/support/のshared examplesを併用するspec
mkdir ruby-smarter-testing && cd ruby-smarter-testing

Gemfileには、RSpecとJUnit形式の結果を出力するフォーマッターを入れます。Smarter Testingはrunの結果をJUnit XMLで受け取るため、rspec_junit_formatterが必要です。

Gemfile
source "https://rubygems.org"

gem "rspec"
gem "rspec_junit_formatter"

依存を入れてから、RSpecの初期ファイルを作ります。bundle installのあとにrspec --initです。

$ bundle install
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Bundle complete! 2 Gemfile dependencies, 7 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

$ bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb

.rspecには--require spec_helperが入るので、各specでspec_helperをrequireする必要はありません。spec/spec_helper.rbは、rspec --initが生成したコメント付きの設定一式のままです。この記事では、そのファイルを丸ごと差し替えず、必要な行だけ足していきます。

続けてディレクトリとソースを作ります。

mkdir -p lib spec/support spec/fixtures .circleci

ソースファイルを3つ作ります。Roundingは、shared examplesの中からしか呼ばれないクラスです。

lib/price.rb
class Price
  def self.with_tax(amount) = (amount * 1.1).round
end

Discountは会員ランク文字列から割引率を返します。

lib/discount.rb
class Discount
  RATES = { "gold" => 0.2, "silver" => 0.1, "none" => 0.0 }

  def self.rate(rank)
    RATES.fetch(rank)
  end
end

Roundingは金額の端数処理だけを担当します。

lib/rounding.rb
class Rounding
  def self.apply(amount)
    amount.round
  end
end

Discount.rateは複数行のdefで書いてください。手元の検証では、def self.rate(rank) = RATES.fetch(rank)のように1行のendless methodで定数Hashのfetchを呼ぶと、Rubyのカバレッジに呼び出しが記録されませんでした。このときdiscount_data_spec.rbとlib/discount.rbの関係はimpact dataに残りません。

生成されたspec_helper.rbに、spec/support/配下を読み込む行を足します。

spec/spec_helper.rb
 # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+Dir[File.join(__dir__, "support", "**", "*.rb")].sort.each { |f| require f }
+
 RSpec.configure do |config|

あとの手順で選ばれる/選ばれない差を見るため、書き方の違う4つのspecと、それらが読むfixture・shared examplesを用意します。

まずPriceを直接呼ぶ、ふつうのspecです。

spec/price_spec.rb
require_relative "../lib/price"

RSpec.describe(Price) { it("100円は税込110円") { expect(Price.with_tax(100)).to eq(110) } }

次は配列のループでexampleを生成するspecです。カバレッジ上は、ふつうのspecと同じくlib/price.rbとの関係が残ります。

spec/price_loop_spec.rb
require_relative "../lib/price"

RSpec.describe "ループで生成" do
  [[200, 220], [300, 330]].each do |amount, expected|
    it("#{amount}円は税込#{expected}円") { expect(Price.with_tax(amount)).to eq(expected) }
  end
end

YAMLの行ごとにexampleを作るspecと、その入力です。あとでYAMLだけを変えたときに、選ばれない側になります。

spec/discount_data_spec.rb
require_relative "../lib/discount"
require "yaml"

RSpec.describe "YAMLから生成" do
  YAML.load_file(File.join(__dir__, "fixtures", "discount_cases.yml")).each do |c|
    it("#{c["rank"]}の割引率は#{c["rate"]}") { expect(Discount.rate(c["rank"])).to eq(c["rate"]) }
  end
end

discount_data_spec.rbが読むfixtureはrankとrateの行です。

spec/fixtures/discount_cases.yml
- { rank: gold, rate: 0.2 }
- { rank: silver, rate: 0.1 }

最後に、ローカルのitとspec/support/のshared examplesを併用するspecです。shared側だけがRoundingを呼びます。

spec/price_shared_spec.rb
require_relative "../lib/price"

RSpec.describe("shared_examplesで生成") do
  it("ローカル固定5000円は5500円") { expect(Price.with_tax(5000)).to eq(5500) }
  it_behaves_like "税込価格", 1000, 1100
end

shared examplesの定義はspec/support/に書きます。

spec/support/shared_price_examples.rb
require_relative "../../lib/rounding"

RSpec.shared_examples "税込価格" do |amount, expected|
  it("#{amount}円は税込#{expected}円") { expect(Rounding.apply(Price.with_tax(amount))).to eq(expected) }
end

全specが通ることを確認します。次の出力なら準備は完了です。

$ bundle exec rspec
.......

Finished in 0.00222 seconds (files took 0.0638 seconds to load)
7 examples, 0 failures

最後にGitリポジトリとして初期化し、コミットしておきます。あとの差分確認のためです。

git init
git add -A && git commit -m "initial"

プラグインを入れ、カバレッジを確かめる

rspec-circleci-coverageを入れます。bundle addがGemfileへの追加とインストールをまとめてやってくれます。

$ bundle add rspec-circleci-coverage
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Fetching rspec-circleci-coverage 0.1.659
Installing rspec-circleci-coverage 0.1.659

Gemfileには次の行が足されます。バージョン制約はインストール時点のものです。

gem "rspec-circleci-coverage", "~> 0.1.659"

先ほど足したsupportの読み込みより前で、プラグインを読み込みます。pluginsブロックはrequire行の直前に挿入します。

spec/spec_helper.rb
+require "rspec-circleci-coverage"
+
 Dir[File.join(__dir__, "support", "**", "*.rb")].sort.each { |f| require f }

 RSpec.configure do |config|

プラグインがカバレッジを記録するのは、環境変数CIRCLECI_COVERAGEが設定されたときだけです。普段のbundle exec rspecの動きは変わりません。公式READMEと同じ手順で、一度カバレッジを出してみましょう。

$ CIRCLECI_COVERAGE=coverage.json bundle exec rspec
rspec-circleci-coverage: generating CircleCI coverage JSON...
.......rspec-circleci-coverage: wrote coverage.json

Finished in 0.01288 seconds (files took 0.09192 seconds to load)
7 examples, 0 failures

$ cat coverage.json | jq .
{
  "lib/discount.rb": {
    "spec/discount_data_spec.rb!!YAMLから生成 goldの割引率は0.2|run": true,
    "spec/discount_data_spec.rb!!YAMLから生成 silverの割引率は0.1|run": true
  },
  "lib/price.rb": {
    "spec/price_loop_spec.rb!!ループで生成 200円は税込220円|run": true,
    "spec/price_loop_spec.rb!!ループで生成 300円は税込330円|run": true,
    "spec/price_shared_spec.rb!!shared_examplesで生成 ローカル固定5000円は5500円|run": true,
    "spec/support/shared_price_examples.rb!!shared_examplesで生成 behaves like 税込価格 1000円は税込1100円|run": true,
    "spec/price_spec.rb!!Price 100円は税込110円|run": true
  },
  "spec/support/shared_price_examples.rb": {
    "spec/support/shared_price_examples.rb!!shared_examplesで生成 behaves like 税込価格 1000円は税込1100円|run": true
  },
  "lib/rounding.rb": {
    "spec/support/shared_price_examples.rb!!shared_examplesで生成 behaves like 税込価格 1000円は税込1100円|run": true
  }
}

lib/price.rbは各specに紐付いています。一方lib/rounding.rbの紐付け先はspec/support/shared_price_examples.rbです。discoverが拾うatomは*_spec.rbだけなので、この紐付けは後述のimpact dataには残りません。このcoverage.jsonは確認用なので、.gitignoreに入れておきます。

echo coverage.json >> .gitignore

test-suites.ymlを書き、doctorで確かめる

.circleci/test-suites.ymlを作ります。

.circleci/test-suites.yml
---
name: rspec tests
discover: find spec -type f -name '*_spec.rb'
run: bundle exec rspec --format RspecJunitFormatter --out "<< outputs.junit >>" << test.atoms >>
analysis: CIRCLECI_COVERAGE="<< outputs.circleci-coverage >>" bundle exec rspec << test.atoms >>
outputs:
  junit: test-reports/junit.xml
options:
  test-impact-analysis: true

discoverはspecファイルを列挙し、runは渡されたspecを実行してJUnit XMLを書き出します。この2つの組み立て方は、test-suites.ymlでテスト分割を始める記事で紹介しています。

Test Impact Analysisのために足すのがanalysisです。runとほぼ同じコマンドですが、CIRCLECI_COVERAGEに<< outputs.circleci-coverage >>を渡す点だけが異なります。プラグインがこの出力先に書いたカバレッジから、CircleCIはspecとファイルの対応関係(impact data)を作ります。optionsのtest-impact-analysis: trueで機能を有効にしてください。

設定を書いたらdoctorを実行します。

$ circleci testsuite doctor "rspec tests" --json
checking test-suite configuration exists...
checking detect test runner...
checking test-suite configuration is valid...
checking discover command can discover test atoms...
checking run command can run discovered test atoms...
checking file-mapper command can map test atoms to files...
checking analysis command can analyze test atoms...
{
  "checks": [
    ...
    {
      "name": "discover command can discover test atoms",
      "output": "Discovered 4 test atoms",
      "state": "success"
    },
    {
      "name": "run command can run discovered test atoms",
      "output": "Ran 4 test atoms",
      "state": "success"
    },
    {
      "name": "file-mapper command can map test atoms to files",
      "output": "check skipped: all test atoms are files, file-mapper is not needed",
      "state": "skipped"
    },
    {
      "name": "analysis command can analyze test atoms",
      "output": "4 test atoms are impacted by source files",
      "state": "success"
    }
  ],
  "action_items": [],

最後のanalysisのチェックがsuccessで、action_itemsが空になっていれば設定は完成です。analysisを書き忘れた場合は、The 'analysis' command is missing when 'test-impact-analysis' is enabled.というaction itemが出て、RSpec用のプラグインの入れ方が案内されます。

impact dataを作り、関係するspecだけが選ばれることを確かめる

ここまでの変更をコミットしてから、--analyze-tests=impactedを付けてtestsuiteを実行します。

git add -A && git commit -m "Configure test impact analysis"
circleci testsuite run "rspec tests" --analyze-tests=impacted --local

--localを付けると、impact dataはCircleCIへ送らず、手元の.circleci/impact-default.jsonへの保存になります。付けずに実行すると、CircleCIのプロジェクトIDと認証を求められて止まります。

Missing required flags when '--local' is not set: --project-id=UUID

初回はimpact dataが無いので全specが選ばれる状態です。そのあとでanalysisが走ります。

Selecting tests...
Selecting all test atoms, no impact analysis available
Selected 4 test atoms, Skipped 0 test atoms in 0s

Running 4 test atoms
...
Analyzing 4 test atoms
...
Found 1 files impacting test spec/price_loop_spec.rb
Found 1 files impacting test spec/discount_data_spec.rb
Found 1 files impacting test spec/price_spec.rb
Found 1 files impacting test spec/price_shared_spec.rb
Updated test impact data in 0s

Found 1 filesは、spec自身を除いて、そのspecの実行したソースファイルが1つ記録されたという意味です。price_spec.rb、price_loop_spec.rb、price_shared_spec.rbはlib/price.rbに、discount_data_spec.rbはlib/discount.rbに紐付きます。

--localでは、この対応関係が.circleci/impact-default.jsonに保存されます(確認用のファイルなので、コミットしないでください)。中身はだいたい次の形です。filesがパスとハッシュの一覧、edgesがdiscoverで得たatom(specファイル)から、そのatomが実行したファイルへの対応です。数値はfilesのIDを指します。

{
  "files": {
    "1": { "path": "lib/price.rb", "hash": "..." },
    "3": { "path": "lib/discount.rb", "hash": "..." },
    "5": { "path": "spec/price_spec.rb", "hash": "..." }
  },
  "edges": {
    "spec/price_spec.rb": [1, 5],
    "spec/price_loop_spec.rb": [2, 1],
    "spec/price_shared_spec.rb": [1, 8],
    "spec/discount_data_spec.rb": [3, 4]
  }
}

price_spec.rbの[1, 5]はlib/price.rbと自分自身、discount_data_spec.rbの[3, 4]はlib/discount.rbと自分自身です。一方、このグラフにlib/rounding.rbとspec/fixtures/discount_cases.ymlは出てきません。先ほどのcoverage.jsonではlib/rounding.rbがspec/support/shared_price_examples.rbに紐付いていましたが、discoverのatomは*_spec.rbだけなので、その関係はimpact dataに残りません。この差が、次の節で「選ばれない」動きにつながります。

変更が無い状態で、選ばれるspecの一覧を見てみます。list-testsはspecを実行せず、選ばれる一覧だけを表示します。何も出ません。

$ circleci testsuite list-tests "rspec tests" --local

では、lib/price.rbを変えてみましょう。計算結果を変えない次の変更をします。

lib/price.rb
 class Price
-  def self.with_tax(amount) = (amount * 1.1).round
+  def self.with_tax(amount) = (amount * 1.1).round + 0
 end

同じコマンドをもう一度実行します。今度は選ばれる一覧が出ます。

$ circleci testsuite list-tests "rspec tests" --local
spec/price_loop_spec.rb
spec/price_spec.rb
spec/price_shared_spec.rb

Priceを呼んでいる3つのspecが選ばれ、Priceを使わないdiscount_data_spec.rbは外れました。runで実行すると、選択の内訳と、実際に渡されたコマンドが見えます。

$ circleci testsuite run "rspec tests" --local
Selecting tests...
Found test impact version: 1
Using 'impact-key' 'default'
- 0 new test atoms
- 0 test atoms impacted by new files
- 3 test atoms impacted by modified files
- 0 test atoms impacted by removed files
- 0 test atoms failed previously
- 0 test atoms with no source file mappings in impact data
- 0 test atoms impacted by include rule
- 0 test atoms impacted by full test run paths
Selected 3 test atoms, Skipped 1 test atoms in 1ms

Running 3 test atoms
[2]    bundle exec rspec --format RspecJunitFormatter --out "test-reports/junit-1.xml" spec/price_loop_spec.rb spec/price_spec.rb spec/price_shared_spec.rb
Ran 3 test atoms in 241ms
Analysis not configured
Not updating test impact data, analysis not enabled

内訳のimpacted by modified filesが、変更したソースに紐付く選択です。Runningの行では、test-suites.ymlのrunにある<< test.atoms >>が、選ばれた3つのspecパスに置き換わっています。ループでexampleを生成するprice_loop_spec.rbも、ふつうのspecと同じく選ばれています。確認が終わったら変更を戻してください。

git checkout -- lib/price.rb

CIでも同じ仕組みです。impact dataがCircleCI側にあれば、フィーチャーブランチでは変更に関係するspecだけが選ばれます。一方、デフォルトブランチでは既定で全specが実行され、あわせてimpact dataが更新されます。

shared examplesとYAMLを使うspecは選ばれない

次は、shared examplesの中でしか使わないlib/rounding.rbを変えます。

lib/rounding.rb
 class Rounding
   def self.apply(amount)
-    amount.round
+    (amount + 0).round
   end
 end

Roundingだけを変えた場合、testsuiteは4 atomすべてをスキップします。

$ circleci testsuite list-tests "rspec tests" --local
$ circleci testsuite run "rspec tests" --local
...
- 0 test atoms impacted by modified files
...
Selected 0 test atoms, Skipped 4 test atoms in 0s

list-testsは何も表示せず、runも4つのspecをすべてスキップしました。price_shared_spec.rbはit_behaves_likeでRoundingを実行しているのに、選ばれていません。

先ほどのcoverage.jsonでは、lib/rounding.rbの紐付け先がspec/support/shared_price_examples.rb側のatomだけになっています。

  "lib/rounding.rb": {
    "spec/support/shared_price_examples.rb!!shared_examplesで生成 behaves like 税込価格 1000円は税込1100円|run": true
  }

discoverが拾うatomは*_spec.rbだけなので、この関係はimpact dataに入りません。次の節で宣言して塞ぎます。

変更を戻して、今度はYAMLに誤った行を1つ足します。noneの割引率は0.0なので、この行のexampleは失敗するはずです。

git checkout -- lib/rounding.rb

続けてfixtureに誤った行を足します。

spec/fixtures/discount_cases.yml
 - { rank: gold, rate: 0.2 }
 - { rank: silver, rate: 0.1 }
+- { rank: none, rate: 0.5 }

fixtureに誤った行を足しただけでは、YAMLはカバレッジに載らずatomは選ばれません。

$ circleci testsuite run "rspec tests" --local
...
Selected 0 test atoms, Skipped 4 test atoms in 0s

discount_data_spec.rbは選ばれず、失敗は見つかりません。Rubyのカバレッジは実行されたRubyコードの行を記録するもので、読み込んだYAMLの中身は対象外だからです。

--run-tests=allで全specを実行すると、この誤りが見つかります。

$ circleci testsuite run "rspec tests" --local --run-tests=all
...
Running all test atoms, --run-tests set to 'all'
...
### Failed test atoms (1)

**spec/discount_data_spec.rb**
- spec.discount_data_spec > YAMLから生成 noneの割引率は0.5

CIに置き換えると、フィーチャーブランチのテストは成功し、誤りが見つかるのはマージ後にデフォルトブランチで全specが実行されたときです。YAMLを元に戻しておきます。

git checkout -- spec/fixtures/discount_cases.yml

test-selection-rulesで依存を宣言する

カバレッジで追えない関係は、optionsのtest-selection-rulesで宣言します。test-atomに書いたspecが、includeに書いたファイルの変更でも選ばれるようになります。

まず、宣言候補のspecをgrepで洗い出します。ヒットするのはshared examplesとYAML読み込みの2本だけです。

$ grep -rlE "it_behaves_like|include_examples|it_should_behave_like" spec --include='*_spec.rb'
spec/price_shared_spec.rb

$ grep -rlE "YAML\.(load_file|safe_load_file)|File\.read|JSON\.parse|CSV\.(read|foreach)" spec --include='*_spec.rb'
spec/discount_data_spec.rb

ヒットしたspecごとに、includeに書くパスを中身から特定します。shared examples側は、呼んでいる名前から定義ファイルを探し、そこがrequireしているソースも拾います。

$ grep -nE "it_behaves_like|include_examples|it_should_behave_like" spec/price_shared_spec.rb
5:  it_behaves_like "税込価格", 1000, 1100

$ grep -rn 'shared_examples "税込価格"' spec
spec/support/shared_price_examples.rb:3:RSpec.shared_examples "税込価格" do |amount, expected|

$ grep -nE "require|require_relative" spec/support/shared_price_examples.rb
1:require_relative "../../lib/rounding"

データファイル側は、読み込み行の引数を実パスに直してください。load_fileの第1引数がfixtureパスです。

$ grep -nE "YAML\.(load_file|safe_load_file)|File\.read|JSON\.parse|CSV\.(read|foreach)" spec/discount_data_spec.rb
5:  YAML.load_file(File.join(__dir__, "fixtures", "discount_cases.yml")).each do |c|

__dir__はspec/なので、実パスはspec/fixtures/discount_cases.ymlです。ここまでで揃ったパスをtest-selection-rulesに書きます。

.circleci/test-suites.yml
 options:
   test-impact-analysis: true
+  test-selection-rules:
+    - test-atom: spec/discount_data_spec.rb
+      include:
+        - spec/fixtures/*.yml
+    - test-atom: spec/price_shared_spec.rb
+      include:
+        - spec/support/*.rb
+        - lib/rounding.rb

price_shared_spec.rbには、shared examplesの定義ファイルに加えてlib/rounding.rbも並べています。includeに書いたファイルの変更だけが選ばれるため、spec/support/*.rbだけではlib/rounding.rbの変更では選ばれません。

test-suites.ymlを変えたら、impact dataを作り直してください。test-suites.yml自体の変更は全specを実行する扱いになるため、作り直さないと次の実行でも全specが選ばれます。

$ circleci testsuite run "rspec tests" --analyze-tests=impacted --local
...
Selecting all test atoms, 'full-test-run-paths' detected modified file: .circleci/test-suites.yml
...

宣言が効くかを確かめるため、先ほど選ばれなかった2つの変更を同時に入れます。今度は結果が壊れる内容です。

lib/rounding.rb
 class Rounding
   def self.apply(amount)
-    amount.round
+    (amount + 1).round
   end
 end

あわせてfixtureに誤った行を1行足します。

spec/fixtures/discount_cases.yml
 - { rank: gold, rate: 0.2 }
 - { rank: silver, rate: 0.1 }
+- { rank: none, rate: 0.5 }

includeルールを足したうえで同じ変更を入れると、impacted by include ruleで2 atomが選ばれます。

$ circleci testsuite run "rspec tests" --local
Selecting tests...
Found test impact version: 1
Using 'impact-key' 'default'
- 0 new test atoms
- 0 test atoms impacted by new files
- 0 test atoms impacted by modified files
- 0 test atoms impacted by removed files
- 0 test atoms failed previously
- 0 test atoms with no source file mappings in impact data
- 2 test atoms impacted by include rule
- 0 test atoms impacted by full test run paths
Selected 2 test atoms, Skipped 2 test atoms in 2ms

Running 2 test atoms
[2]    bundle exec rspec --format RspecJunitFormatter --out "test-reports/junit-1.xml" spec/discount_data_spec.rb spec/price_shared_spec.rb
Ran 2 test atoms in 284ms
Analysis not configured
Not updating test impact data, analysis not enabled
'run' [2] CircleCI received a non-zero exit code from the test runner: exit status 1

### Failed test atoms (2)

**spec/discount_data_spec.rb**
- spec.discount_data_spec > YAMLから生成 noneの割引率は0.5

**spec/price_shared_spec.rb**
- spec.price_shared_spec > shared_examplesで生成 behaves like 税込価格 1000円は税込1100円

内訳のimpacted by include ruleが2件で、宣言した2つのspecだけが選ばれ、<< test.atoms >>にもその2つが渡りました。どちらも失敗したので、確認が終わったら次のコマンドで戻してください。

git checkout -- lib/rounding.rb spec/fixtures/discount_cases.yml

YAMLを戻したあとの最初の実行では、直前に失敗したspecがfailed previouslyとして数えられ、変更が無くても選ばれます。失敗修復の確認用に用意された挙動です。

includeのglobは、実在するファイルに1件もマッチしないと設定エラーになります。手元の検証では、spec/fixtures/直下にしかYAMLが無い状態でspec/fixtures/**/*.ymlと書いたところ、doctorが次のエラーで止まりました。

unknown failure running check: error in include rule for 'spec/discount_data_spec.rb': no matches for glob 'spec/fixtures/**/*.yml'

**に限らず、マッチが0件になるglobはすべて同じエラーです。実在するパスに合わせて書いてください。

特定のspecに限らず全体の動作に影響するファイルは、full-test-run-pathsに書きます。何も書かない場合は.circleci/test-suites.ymlとGemfile.lockが対象で、どちらかが変わると、選択の内訳にimpacted by full test run pathsと表示されて全specが選ばれます。DBマイグレーションなどを足すときは、明示したリストが既定値を置き換えるため、既定の2つも一緒に書いてください。

.circleci/test-suites.yml(optionsに追加)
  full-test-run-paths:
    - .circleci/test-suites.yml
    - Gemfile.lock
    - db/migrate/*.rb

CircleCIで動かす

doctorの案内どおり、CIではテストの実行ステップをcircleci testsuite runに置き換えます。JUnit XMLの取り込みもtestsuiteに任せるため、test-suites.ymlのoptionsにstore-test-results: trueを足し、store_test_resultsステップは使いません。

.circleci/test-suites.yml
 options:
   test-impact-analysis: true
+  store-test-results: true

CIではbundle installのあとcircleci testsuite runだけを実行するジョブに置き換えます。

.circleci/config.yml
version: 2.1

jobs:
  test:
    docker:
      - image: cimg/ruby:4.0
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: bundle install
      - run:
          name: Run testsuite
          command: circleci testsuite run "rspec tests" --analyze-tests=impacted

workflows:
  ci-tests:
    jobs:
      - test

--localを付けないので、impact dataはCircleCI側に保存されます。手元の.circleci/impact-*.jsonは確認用なので、コミットしないでください。あわせて.circleci/state.jsonもローカル実行の状態用なので、.gitignoreに.circleci/impact-*.jsonと.circleci/state.jsonを入れておくと安心です。

デフォルトブランチでは--analyze-testsは既定でimpacted、フィーチャーブランチではnoneです。前の節では手元で--analyze-tests=impactedを付けてimpact dataを作りましたが、上のconfig.ymlのとおりフラグを省略すると、フィーチャーブランチのビルドではanalysisが走りません。デフォルトブランチのビルドがまだ無い導入直後はimpact dataが増えず、先ほどのSelecting all test atoms, no impact analysis availableのままに見えがちです。導入の最初期だけ--analyze-tests=impactedを付けてください。analysisはカバレッジ付きの実行になる分、初回は特にジョブが長くなります。デフォルトブランチでimpact dataが更新されるようになったら、フラグを外して既定に戻します。

最初のフィーチャーブランチより前にimpact dataを用意したい場合は、手元からCircleCIへ送る実行です。--localを外すと--project-idが必須です。circleci auth loginで認証してから、次のように実行します。

circleci auth login
export PROJECT_ID=$(circleci project get --json | jq -r .id)
circleci testsuite run "rspec tests" --analyze-tests=impacted --project-id="$PROJECT_ID"

circleci project getは、作業ディレクトリのgit remoteからプロジェクトを解決します。CircleCI側でプロジェクトをフォロー済みであることが前提です。

まとめ

RSpecでTest Impact Analysisを使うには、rspec-circleci-coverageと.circleci/test-suites.ymlのanalysisを用意し、impact dataを作ります。カバレッジに載る関係なら、ソースを変えたときに関係するspecだけが選ばれます。

一方、shared examplesの定義ファイルやYAMLのようなデータファイルへの依存はカバレッジだけでは残りません。選ばれない変更は、specの読み込み先を辿ってtest-selection-rulesのincludeに書きます。宣言のあとは、list-testsやrunの内訳で「意図したspecだけが選ばれているか」を確認してください。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?