テストにおける環境変数の一時的な上書きを共通ヘルパーに集約

puma/puma

Pumaのテストコードで重複していた環境変数の一時保存・復元パターンを、with_temp_env ヘルパーメソッドに集約しました。これにより、テストコード間の一貫性が向上し、環境変数を扱う新規テストの実装が簡潔になります。

背景

Pumaのテストコード内には、環境変数を一時的に設定して ensure ブロックで元に戻すパターンが複数箇所で重複していました。#3875 で報告された主な重複箇所は以下の3つです:

  • test/test_log_writer.rb: ENV["PUMA_DEBUG"] の保存・復元
  • test/test_error_logger.rb: with_debug_mode ヘルパーでの ENV["PUMA_DEBUG"] ラッピング
  • test/test_cli.rb: APP_ENV / RAILS_ENV の設定と ensure でのクリーンアップ

これらは同じ目的のコードが異なる形で実装されており、新しいテストを追加する際にどのパターンに従うべきか不明瞭でした。本PRは、このパターンを共通化して保守性を向上させています。

技術的な変更

with_temp_env メソッドの追加

test/helper.rb に環境変数の一時的な設定と復元を行う共通ヘルパーが追加されました。このメソッドは2つのパラメータを受け取ります:

def with_temp_env(temp_env, del_env={}, &block)
  original_env = {}

  temp_env.transform_keys(&:to_s).each do |k, v|
    original_env[k], ENV[k] = ENV[k], v
  end

  del_env.transform_keys(&:to_s).each { |k, v| ENV[k] = v }

  yield
ensure
  # Restore original values
  original_env.each { |k, v| ENV[k] = v }

  # Remove keys that were added via del_env
  del_env.transform_keys(&:to_s).each { |k, _| ENV.delete(k) }
end

第1引数 temp_env は既存の環境変数を一時的に上書きし、ブロック実行後に元の値へ復元します。第2引数 del_env は新しい環境変数を追加し、ブロック実行後に削除します。この2つのパラメータにより、「既存の値を保存して復元」と「一時的に追加して削除」の両方のパターンに対応しています。

既存テストでの適用例

test/test_cli.rbtest_environment_rails_env では、以下のように書き換えられました:

変更前:

def test_environment_rails_env
  ENV.delete 'RACK_ENV'
  ENV['RAILS_ENV'] = @environment

  cli = Puma::CLI.new []

  conf = cli.instance_variable_get(:@conf)
  conf.clamp

  assert_equal @environment, conf.environment
ensure
  ENV.delete 'RAILS_ENV'
end

変更後:

def test_environment_rails_env
  ENV.delete 'RACK_ENV'

  with_temp_env({}, { "RAILS_ENV": @environment }) do
    cli = Puma::CLI.new []

    conf = cli.instance_variable_get(:@conf)
    conf.clamp

    assert_equal @environment, conf.environment
  end
end

del_env パラメータに RAILS_ENV を渡すことで、ブロック終了時に自動的に削除されます。ensure ブロックの記述が不要になり、テストのロジックに集中できます。

test/test_error_logger.rb では、ローカルメソッド with_debug_mode が削除され、共通の with_temp_env に置き換えられました:

変更前:

def test_debug_with_debug_mode
  with_debug_mode do
    _, err = capture_io do
      Puma::ErrorLogger.stdio.debug(text: 'non-blank')
    end

    assert_includes err, '% non-blank'
  end
end

private

def with_debug_mode
  original_debug, ENV["PUMA_DEBUG"] = ENV["PUMA_DEBUG"], "1"
  yield
ensure
  ENV["PUMA_DEBUG"] = original_debug
end

変更後:

def test_debug_with_debug_mode
  with_temp_env({ "PUMA_DEBUG": "1" }) do
    _, err = capture_io do
      Puma::ErrorLogger.stdio.debug(text: 'non-blank')
    end

    assert_includes err, '% non-blank'
  end
end

テストクラスごとに定義されていたプライベートメソッドが不要になり、コードの重複が解消されています。

ヘルパーメソッドのテスト

test/test_helper.rbwith_temp_env 自体の動作を検証するテストが追加されました:

def test_with_temp_env
  original_puma_debug_env = ENV["PUMA_DEBUG"]

  with_temp_env({ "PUMA_DEBUG": "1" }, { "APP_ENV" => "test" }) do
    refute_equal original_puma_debug_env, ENV["PUMA_DEBUG"]
    assert_equal "1", ENV["PUMA_DEBUG"]
    assert_equal "test", ENV["APP_ENV"]
  end

  assert_operator original_puma_debug_env, :==, ENV["PUMA_DEBUG"]
  refute ENV.key?("APP_ENV"), "Expected the APP_ENV key to be removed"
end

ブロック内での値の上書きと、ブロック終了後の復元・削除が正しく動作することを確認しています。ヘルパーメソッド自体の品質を保証するテストケースです。

設計判断

2つのパラメータによる使い分け

temp_envdel_env という2つのパラメータを設けることで、「既存の値を一時的に変更」と「新しい値を一時的に追加」の両方のユースケースに対応しています。test_environment_app_env では両方のパターンが混在しています:

with_temp_env(
  { "RACK_ENV": @environment },
  { "RAILS_ENV": @environment, "APP_ENV": "test" }
) do
  cli = Puma::CLI.new []
  conf = cli.instance_variable_get(:@conf)
  conf.clamp
  assert_equal 'test', conf.environment
end

RACK_ENV は元の値を保存して後で復元し、RAILS_ENVAPP_ENV はテスト後に削除されます。この設計により、単一のヘルパーで複雑な環境変数の操作パターンにも対応できます。

Symbol/String両対応

transform_keys(&:to_s) により、ハッシュのキーがSymbolでもStringでも統一的に処理されます。テストコードでは { "PUMA_DEBUG": "1" } のようにSymbolを使う例と { "APP_ENV" => "test" } のようにStringを使う例の両方が存在しますが、ヘルパー内で統一されるため呼び出し側は自由に選択できます。

まとめ

本PRは、テストコード間で重複していた環境変数の保存・復元パターンを with_temp_env ヘルパーに集約し、コードの一貫性と保守性を向上させました。2つのパラメータによる使い分けとキーの型変換により、既存の多様なパターンに対応しつつ、新しいテストでの利用も簡潔にしています。ヘルパー自体のテストケースも追加され、安全な環境変数操作が保証されています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→セクション群(各論)→まとめ(結論)の3部構成が明確に適用されています。「設計判断」セクションも含まれており、読者の深い理解を促す優れた構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とGitHubのPR/Issueへのリンク記法が、ガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Pumaのテストコードに関するリファクタリングという専門的な内容を、過度な説明なく簡潔に記述しており、対象読者であるエンジニアに適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフの冒頭で要点を述べる「トピックセンテンス先頭」の原則が徹底されており、非常に読みやすい構造になっています。1段落1トピックも遵守されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているすべてのコードブロック(変更前・変更後を含む)は、提供されたDiff情報と完全に一致しており、正確に内容を反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ensureブロック」「ヘルパーメソッド」「環境変数」などの技術用語が、文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

新設された`with_temp_env`メソッドの2つのパラメータ(`temp_env`, `del_env`)の役割や、`transform_keys`の働きについての説明は、コードの挙動と完全に一致しており技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内のコードといった提供情報で裏付けられています。「設計判断」セクションの内容もコードから読み取れる事実に基づいた解説であり、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#3879)、Issue番号(#3875)、ファイルパス、メソッド名などの数値・固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「テストにおける環境変数の一時的な上書きを共通ヘルパーに集約」は、PRの主題(add shared test ENV helper)を的確に、かつ分かりやすく表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はPR情報とDiffの範囲内に留まっており、バージョンのサポート状況など、PRに記載のない外部知識の持ち込みはありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜しました」「〜されました」といった過去形の表現が適切に使用されており、完了した変更であることが正しく伝わります。時間表現の歪曲はありません。