상황
외부 공급자 결과를 모으는 작은 수집기 모듈에 테스트 12개가 붙어 있었다. 작업 셸에서는 모두 통과했다. CI에서는 11개가 실패했다. 실패 메시지는 테스트 내용과 상관없이 모두 같았다. 공급자 API 키 환경 변수가 설정되지 않았다는 예외였다.
원인
모듈이 로드되거나 생성될 때 키가 있는지 확인하고, 없으면 바로 예외를 던졌다. 그 자체는 실제 실행에서는 친절한 동작이다. 문제는 테스트가 그 키를 쓰지 않는데도 같은 경로를 지나간다는 것이었다. 작업하던 셸에는 평소에 쓰던 키가 내보내져 있었고, CI 러너에는 당연히 없었다. 로컬의 초록불은 코드가 맞다는 증거가 아니라 내 셸에 키가 있다는 증거였다.
팁: 사전 확인은 진입점에서
"키가 없으면 실행하지 않는다" 같은 사전 확인은 사용자가 실제로 실행하는 CLI 스크립트나 서버 시작 지점에 둔다. 라이브러리 모듈 안에서 import 시점에 환경을 검사하면, 그 모듈을 불러오는 모든 테스트가 환경에 묶인다. 진입점에서 확인하면 실제 사용자는 여전히 명확한 오류를 받고, 테스트는 영향을 받지 않는다.
팁: 테스트가 환경을 직접 정한다
환경 변수에 의존하는 테스트는 그 값을 테스트 안에서 설정하고, 끝나면 원래대로 돌려놓는다. 반대로 "키가 없을 때 오류가 나는가"를 확인하는 테스트는 그 변수를 명시적으로 지운 상태에서 돈다. 어느 쪽이든 실행하는 사람의 셸에 무엇이 들어 있는지에 따라 결과가 바뀌면 안 된다.
팁: 로컬에서 깨끗한 환경으로 한 번
푸시하기 전에 env -u <변수명> <테스트 명령>처럼 의심 가는 변수를 빼고 한 번 돌린다. 더 확실하게는 env -i PATH="$PATH" HOME="$HOME" <테스트 명령>으로 거의 빈 환경에서 돌려 본다. CI에서 처음 발견할 실패를 몇 초 만에 로컬에서 볼 수 있다.
체크리스트
1) 실패 메시지가 테스트마다 같다면 환경부터 의심한다. 2) 모듈 로드 시점의 환경 검사를 찾는다. 3) 사전 확인을 진입점으로 옮긴다. 4) 테스트가 쓰는 환경 변수를 테스트 안에서 정한다. 5) 키를 뺀 환경에서 로컬로 한 번 돌린다.
What happened
A small collector module that gathers results from an external provider had twelve tests. In my working shell they all passed. In CI, eleven failed. The failure message was identical regardless of what each test checked: an exception saying the provider API key variable was not set.
Why
The module checked for the key when it was loaded or constructed and threw immediately if it was missing. In a real run that is a helpful behaviour. The problem was that the tests went through the same path even though none of them used the key. My shell had the key exported from everyday use; the CI runner, naturally, did not. The local green was not evidence that the code was right. It was evidence that my shell had a key.
Tip: put preflight checks at the entry point
Checks like "refuse to run without a key" belong in the CLI script or server start-up that a user actually runs. When a library module inspects the environment at import time, every test that imports it is tied to that environment. Check at the entry point and real users still get a clear error, while tests are unaffected.
Tip: let each test decide its environment
A test that depends on an environment variable sets it inside the test and restores it afterwards. A test that checks "does it fail without the key" explicitly clears the variable first. Either way, the result must not depend on what happens to be in the shell of whoever runs it.
Tip: run once locally with a clean environment
Before pushing, run the suite once with the suspicious variable removed, for example env -u <NAME> <test command>. To be more thorough, run it in a nearly empty environment with env -i PATH="$PATH" HOME="$HOME" <test command>. The failure you would otherwise first see in CI shows up locally in seconds.
Checklist
1) If every test fails with the same message, suspect the environment first. 2) Look for environment checks at module load time. 3) Move preflight checks to the entry point. 4) Have tests set the variables they use. 5) Run once locally with the key removed.
发生了什么
一个从外部服务商汇总结果的小采集器模块,配了 12 个测试。在我工作的 shell 里全部通过;到了 CI 上,挂了 11 个。不管每个测试检查的是什么,失败信息都一模一样:服务商 API 密钥的环境变量没有设置。
原因
这个模块在加载或构造时检查密钥,缺了就立刻抛异常。真正运行时,这是个友好的行为。问题在于测试明明都不用这个密钥,却也走了同一条路径。我的 shell 里因为日常使用早就导出了密钥,CI 运行器里当然没有。本地的绿灯证明的不是代码正确,而是我的 shell 里有密钥。
提示:前置检查放在入口
"没有密钥就不运行"这类前置检查,应该放在用户真正执行的 CLI 脚本或服务启动处。库模块在导入时检查环境,会把所有导入它的测试都绑在那个环境上。放在入口检查,真实用户照样能得到清楚的错误,测试也不受影响。
提示:每个测试自己决定环境
依赖环境变量的测试,在测试内部设置这个值,结束后恢复原样。反过来,检查"缺少密钥时会不会报错"的测试,要先显式清除这个变量。无论哪种,结果都不能取决于运行者的 shell 里碰巧有什么。
提示:本地用干净环境跑一次
推送之前,先去掉可疑的变量跑一遍,比如 env -u <变量名> <测试命令>。想更彻底,就用 env -i PATH="$PATH" HOME="$HOME" <测试命令> 在几乎为空的环境里跑。原本要到 CI 才第一次看到的失败,几秒钟就能在本地看到。
检查清单
1) 如果每个测试的失败信息都一样,先怀疑环境。2) 找出模块加载时的环境检查。3) 把前置检查移到入口。4) 让测试自己设置它用到的变量。5) 本地去掉密钥跑一次。
何が起きたか
外部プロバイダーの結果を集める小さなコレクターモジュールに、テストが 12 件ついていた。作業中のシェルではすべて通った。CI では 11 件が落ちた。各テストが何を確かめているかに関係なく、失敗メッセージは全部同じだった。プロバイダーの API キーの環境変数が設定されていない、という例外だ。
なぜか
モジュールは読み込み時か生成時にキーの有無を確かめ、なければすぐ例外を投げていた。実際の実行ではそれは親切な振る舞いだ。問題は、どのテストもキーを使わないのに同じ経路を通っていたことだった。私のシェルには普段使いのキーがエクスポートされていて、CI のランナーには当然なかった。ローカルの緑はコードが正しい証拠ではなく、私のシェルにキーがある証拠だった。
Tip: 事前チェックは入口に置く
「キーがなければ実行しない」といった事前チェックは、ユーザーが実際に実行する CLI スクリプトやサーバーの起動処理に置く。ライブラリのモジュールが import 時に環境を調べると、それを読み込むすべてのテストがその環境に縛られる。入口で確かめれば、実際のユーザーには分かりやすいエラーが出たまま、テストは影響を受けない。
Tip: 環境は各テストが自分で決める
環境変数に依存するテストは、その値をテストの中で設定し、終わったら元に戻す。逆に「キーがないときにエラーになるか」を確かめるテストは、先にその変数を明示的に消してから走らせる。どちらにしても、実行する人のシェルにたまたま何が入っているかで結果が変わってはいけない。
Tip: ローカルでもきれいな環境で一度
プッシュする前に、怪しい変数を外して一度走らせる。たとえば env -u <変数名> <テストコマンド>。さらに確実にするなら env -i PATH="$PATH" HOME="$HOME" <テストコマンド> でほぼ空の環境で走らせる。CI で初めて見るはずだった失敗が、ローカルで数秒のうちに見える。
チェックリスト
1) どのテストも同じメッセージで落ちるなら、まず環境を疑う。2) モジュール読み込み時の環境チェックを探す。3) 事前チェックを入口に移す。4) テストが使う変数はテストの中で決める。5) キーを外した環境でローカルで一度走らせる。