はじめに:本当にあった怖い話
あなたはチームでプログラム開発をしています。
あなたは開発タスクをアサインされ、開発ブランチで次のようなコードとテストコードを書きました。
class Bar
def greet
'Hello!'
end
end
class BarTest < Minitest::Test
def test_greet
assert_equal 'Hello!', Bar.new.greet
end
end
ローカルのテストは問題なくパスする ✅
単純な変更なので、他の既存のテストに影響はないと思いますが、念のためプロジェクト全体のテストを実行しておきましょう。
$ rake test
Run options: --seed 55749
# Running:
..
Finished in 0.000233s, 8583.6905 runs/s, 8583.6905 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
はい、問題なくすべてのテストがパスしましたね!
でもCI上だと・・・?
では、プルリクエスト(PR)を作りましょう。
このプロジェクトではPRを作るとGitHub Actions(CI)が全テストを実行してくれます。
とはいえ、ローカルで全テストがパスしているので、CIのテストも全部パスするはず・・・あれっ!?
なぜかテストが落ちてしまいました。
実行結果は以下のように、既存のテストであるfoo_test.rbが落ちています。
Run options: --seed 36787
# Running:
F.
Finished in 0.000465s, 4303.1019 runs/s, 4303.1019 assertions/s.
1) Failure:
FooTest#test_calc [test/foo_test.rb:8]:
Expected: 5
Actual: 1
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips
rake aborted!
Command failed with status (1)
調査、調査!!
もしかすると落ちたのは「フレーキーなテスト(たまに落ちるテスト)」なのでしょうか?
とりあえず、テストが落ちてしまったFooクラスとfoo_test.rbを覗いてみましょう。
class Foo
def calc(a, b)
a + b
end
end
class FooTest < Minitest::Test
def test_calc
assert_equal 5, Foo.new.calc(3, 2)
end
end
うーん、どこをどう見ても「これで落ちるわけがない」というぐらい、シンプルなテストですよね。。
しかし、CI上ではテストを何度実行しても毎回同じように落ちてしまいます。
いったい、なんでー!!??
答え:mainブランチのテストが壊れていたから
実はこれ、他の開発者がうっかりテストが壊れた状態のコード(アプリ側のコードだけ修正して、テストを修正していないPR)をmainにマージしてしまったせいです。
PRではベースブランチと仮マージされたコードが実行される
ちなみに、このプロジェクトのGitHub Actionsの設定は以下のようになっています。
name: Test
on:
push:
branches:
- main
# ↓PRが作られるとベースブランチと仮マージしたコードでテストが実行される
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
- run: rake test
上でコメントを入れたとおり、on: pullrequest: が実行のトリガーになっている場合は、PRのベースブランチと仮マージしたコードでテストが実行されます。
ゆえに、CIでテストを実行した場合は「壊れたmainブランチのテスト」と一緒にテストが実行されるため、CIで実行したときだけ、テストが毎回失敗するのでした!!
実務ではどんな感じだったのか
今回は説明用にごくシンプルなコードを使いました。
これぐらいの規模であれば、ちょっと調査すればすぐに原因がわかったかもしれません。
実際、Claude Codeに原因の調査を依頼したら、原因を突き止めてくれました。
しかし、僕が実際に遭遇したのは実務で使っている巨大なコードベースです。
しかも(あまりよくないですが)フレーキーなテストもちょこちょこあるプロジェクトだったので、僕も最初は「フレーキーなテストが原因だろう」と予想していました。
ですが、ローカルでは毎回パスするので「ローカルだとなかなか同じ落ち方をしないな」と首をかしげていました。
また、Claude Codeに原因調査を依頼しても、ずっと右往左往して、いくら待っても原因にたどり着きませんでした。
このときは結局、最新のmainブランチをgit pullしてローカルでテストを回すと、CIと同じようにテストが落ちることに気付き、そこから「壊れたテストをマージしたPR」の存在に気付いたのでした。
「そもそも壊れたテストをmainブランチにマージするなよ」という話ではあるんですが、前述のとおり、このプロジェクトではフレーキーなテストがいくつかあり、なかなか一発で全テストがパスしない状態でした。
なので、チームメンバーは「今回もたまたま落ちただけだろう」と高をくくってマージしてしまったのかもしれません。
よくないですね!
やはりテストは毎回一発で全パスするようにしないといけない!!
というわけで、このあと時間をかけてフレーキーなテストが落ちないように修正を加えていきました。
これでテストの狼少年化は避けられるはずです!
まとめ(今回の教訓)
というわけで、今回の教訓をまとめると以下のようになります。
- ローカルでは毎回パスするが、CIでは毎回落ちるテストがある場合、PRのベースブランチのテストが壊れている可能性がある
- フレーキーなテストを放置してはいけない。放置するとテストが狼少年になってしまい「また落ちてるけど大丈夫だろう」とテストが壊れたままPRをマージしてしまう恐れがある
みなさんも気をつけてくださいね!!
おまけ:サンプルコードはこちら
この記事を書くために作ったサンプルコードはこちらに置いています。
よかったら参考にどうぞ。



