Back to Repositories

Testing Rails Environment Configuration Implementation in dotenv

This test suite validates the Rails-specific functionality of the dotenv gem, focusing on environment variable loading and configuration in a Rails application context. It ensures proper environment file handling and integration with Rails initialization process.

Test Coverage Overview

The test suite provides comprehensive coverage of dotenv-rails functionality:
  • Environment-specific file loading (.env, .env.test, etc.)
  • Spring integration for file watching
  • Rails initialization hooks and configuration
  • Environment variable overwriting behavior
  • Auto-restoration functionality in test environments
  • Logging implementation and customization

Implementation Analysis

The testing approach utilizes RSpec’s powerful features for Rails application testing. It implements around/before hooks for environment isolation, leverages let blocks for shared resources, and uses stub_const for Spring integration testing. The suite demonstrates proper handling of Rails application lifecycle and environment configuration.

Technical Details

Key technical components include:
  • RSpec as the testing framework
  • Rails test environment configuration
  • StringIO for log capture
  • Mock Rails application setup
  • Spring module stubbing
  • Environment variable manipulation

Best Practices Demonstrated

The test suite exemplifies testing best practices through proper isolation of test environments, comprehensive edge case coverage, and clean test organization. It demonstrates effective use of RSpec features, proper setup/teardown patterns, and thorough validation of environment-specific behaviors.

bkeepers/dotenv

spec/dotenv/rails_spec.rb

            
require "spec_helper"
require "rails"
require "dotenv/rails"

describe Dotenv::Rails do
  let(:log_output) { StringIO.new }
  let(:application) do
    log_output = self.log_output
    Class.new(Rails::Application) do
      config.load_defaults Rails::VERSION::STRING.to_f
      config.eager_load = false
      config.logger = ActiveSupport::Logger.new(log_output)
      config.root = fixture_path

      # Remove method fails since app is reloaded for each test
      config.active_support.remove_deprecated_time_with_zone_name = false
    end.instance
  end

  around do |example|
    # These get frozen after the app initializes
    autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
    autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup

    # Run in fixtures directory
    Dir.chdir(fixture_path) { example.run }
  ensure
    # Restore autoload paths to unfrozen state
    ActiveSupport::Dependencies.autoload_paths = autoload_paths
    ActiveSupport::Dependencies.autoload_once_paths = autoload_once_paths
  end

  before do
    Rails.env = "test"
    Rails.application = nil
    Rails.logger = nil

    begin
      # Remove the singleton instance if it exists
      Dotenv::Rails.remove_instance_variable(:@instance)
    rescue
      nil
    end
  end

  describe "files" do
    it "loads files for development environment" do
      Rails.env = "development"

      expect(Dotenv::Rails.files).to eql(
        [
          ".env.development.local",
          ".env.local",
          ".env.development",
          ".env"
        ]
      )
    end

    it "does not load .env.local in test rails environment" do
      Rails.env = "test"
      expect(Dotenv::Rails.files).to eql(
        [
          ".env.test.local",
          ".env.test",
          ".env"
        ]
      )
    end

    it "can be modified in place" do
      Dotenv::Rails.files << ".env.shared"
      expect(Dotenv::Rails.files.last).to eq(".env.shared")
    end
  end

  it "watches other loaded files with Spring" do
    stub_spring(load_watcher: true)
    application.initialize!
    path = fixture_path("plain.env")
    Dotenv.load(path)
    expect(Spring.watcher).to include(path.to_s)
  end

  it "doesn't raise an error if Spring.watch is not defined" do
    stub_spring(load_watcher: false)

    expect {
      application.initialize!
    }.to_not raise_error
  end

  context "before_configuration" do
    it "calls #load" do
      expect(Dotenv::Rails.instance).to receive(:load)
      ActiveSupport.run_load_hooks(:before_configuration)
    end
  end

  context "load" do
    subject { application.initialize! }

    it "watches .env with Spring" do
      stub_spring(load_watcher: true)
      subject
      expect(Spring.watcher).to include(fixture_path(".env").to_s)
    end

    it "loads .env.test before .env" do
      subject
      expect(ENV["DOTENV"]).to eql("test")
    end

    it "loads configured files" do
      Dotenv::Rails.files = [fixture_path("plain.env")]
      expect { subject }.to change { ENV["PLAIN"] }.from(nil).to("true")
    end

    it "loads file relative to Rails.root" do
      allow(Rails).to receive(:root).and_return(Pathname.new("/tmp"))
      Dotenv::Rails.files = [".env"]
      expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false})
      subject
    end

    it "returns absolute paths unchanged" do
      Dotenv::Rails.files = ["/tmp/.env"]
      expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false})
      subject
    end

    context "with overwrite = true" do
      before { Dotenv::Rails.overwrite = true }

      it "overwrites .env with .env.test" do
        subject
        expect(ENV["DOTENV"]).to eql("test")
      end

      it "overwrites any existing ENV variables" do
        ENV["DOTENV"] = "predefined"
        expect { subject }.to(change { ENV["DOTENV"] }.from("predefined").to("test"))
      end
    end
  end

  describe "root" do
    it "returns Rails.root" do
      expect(Dotenv::Rails.root).to eql(Rails.root)
    end

    context "when Rails.root is nil" do
      before do
        allow(Rails).to receive(:root).and_return(nil)
      end

      it "falls back to RAILS_ROOT" do
        ENV["RAILS_ROOT"] = "/tmp"
        expect(Dotenv::Rails.root.to_s).to eql("/tmp")
      end
    end
  end

  describe "autorestore" do
    it "is loaded if RAILS_ENV=test" do
      expect(Dotenv::Rails.autorestore).to eq(true)
      expect(Dotenv::Rails.instance).to receive(:require).with("dotenv/autorestore")
      application.initialize!
    end

    it "is not loaded if RAILS_ENV=development" do
      Rails.env = "development"
      expect(Dotenv::Rails.autorestore).to eq(false)
      expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
      application.initialize!
    end

    it "is not loaded if autorestore set to false" do
      Dotenv::Rails.autorestore = false
      expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
      application.initialize!
    end

    it "is not loaded if ClimateControl is defined" do
      stub_const("ClimateControl", Module.new)
      expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
      application.initialize!
    end

    it "is not loaded if IceAge is defined" do
      stub_const("IceAge", Module.new)
      expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
      application.initialize!
    end
  end

  describe "logger" do
    it "replays to Rails.logger" do
      expect(Dotenv::Rails.logger).to be_a(Dotenv::ReplayLogger)
      Dotenv::Rails.logger.debug("test")

      application.initialize!

      expect(Dotenv::Rails.logger).not_to be_a(Dotenv::ReplayLogger)
      expect(log_output.string).to include("[dotenv] test")
    end

    it "does not replace custom logger" do
      logger = Logger.new(log_output)

      Dotenv::Rails.logger = logger
      application.initialize!
      expect(Dotenv::Rails.logger).to be(logger)
    end
  end

  def stub_spring(load_watcher: true)
    spring = Module.new do
      if load_watcher
        def self.watcher
          @watcher ||= Set.new
        end

        def self.watch(path)
          watcher.add path
        end
      end
    end

    stub_const "Spring", spring
  end
end