Back to Repositories

Testing Exception Handling Workflows in BetterErrors

This test suite validates the RaisedException class in BetterErrors, focusing on exception handling and error message formatting. It verifies proper handling of various exception types including Runtime, SyntaxError, and framework-specific errors like Rails and HAML exceptions.

Test Coverage Overview

The test suite provides comprehensive coverage of exception handling scenarios:

  • Basic RuntimeError handling and message extraction
  • Rails-specific template errors (both Rails 6+ and legacy versions)
  • Syntax errors with backtrace parsing
  • HAML template errors
  • Coffeelint syntax errors
  • Exception hint functionality

Implementation Analysis

The testing approach uses RSpec’s context-based structure to organize different exception scenarios. It leverages RSpec’s subject/let patterns for clean test setup and mock objects through doubles and stubs to isolate test cases.

The implementation makes extensive use of shared examples and before blocks for setup, demonstrating proper test isolation and DRY principles.

Technical Details

  • Testing Framework: RSpec
  • Key Libraries: rspec/its for property testing
  • Mocking: RSpec doubles and stubs
  • Test Organization: Nested contexts and shared behaviors
  • Setup: Modular configuration with spec_helper

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with proper setup and teardown
  • Clear context descriptions and test organization
  • Effective use of RSpec’s DSL features
  • Comprehensive edge case coverage
  • Proper mocking and stubbing techniques

bettererrors/better_errors

spec/better_errors/raised_exception_spec.rb

            
require "spec_helper"
require "rspec/its"

module BetterErrors
  describe RaisedException do
    let(:exception) { RuntimeError.new("whoops") }
    subject(:described_instance) { RaisedException.new(exception) }

    before do
      allow(BetterErrors::ExceptionHint).to receive(:new).and_return(exception_hint)
    end
    let(:exception_hint) { instance_double(BetterErrors::ExceptionHint, hint: nil) }

    its(:exception) { is_expected.to eq exception }
    its(:message)   { is_expected.to eq "whoops" }
    its(:type)      { is_expected.to eq RuntimeError }

    context 'when the exception is an ActionView::Template::Error that responds to #cause (Rails 6+)' do
      before do
        stub_const(
          "ActionView::Template::Error",
          Class.new(StandardError) do
            def cause
              RuntimeError.new("something went wrong!")
            end
          end
        )
      end
      let(:exception) {
        ActionView::Template::Error.new("undefined method `something!' for #<Class:0x00deadbeef>")
      }

      its(:message) { is_expected.to eq "something went wrong!" }
      its(:type) { is_expected.to eq RuntimeError }
    end

    context 'when the exception is a Rails < 6 exception that has an #original_exception' do
      let(:original_exception) { RuntimeError.new("something went wrong!") }
      let(:exception) { double(:original_exception => original_exception) }

      its(:exception) { is_expected.to eq original_exception }
      its(:message) { is_expected.to eq "something went wrong!" }
      its(:type) { is_expected.to eq RuntimeError }
    end

    context "when the exception is a SyntaxError" do
      let(:exception) { SyntaxError.new("foo.rb:123: you made a typo!") }

      its(:message) { is_expected.to eq "you made a typo!" }
      its(:type)    { is_expected.to eq SyntaxError }

      it "has the right filename and line number in the backtrace" do
        expect(subject.backtrace.first.filename).to eq("foo.rb")
        expect(subject.backtrace.first.line).to eq(123)
      end
    end

    context "when the exception is a HAML syntax error" do
      before do
        stub_const("Haml::SyntaxError", Class.new(SyntaxError))
      end

      let(:exception) {
        Haml::SyntaxError.new("you made a typo!").tap do |ex|
          ex.set_backtrace(["foo.rb:123", "haml/internals/blah.rb:123456"])
        end
      }

      its(:message) { is_expected.to eq "you made a typo!" }
      its(:type)    { is_expected.to eq Haml::SyntaxError }

      it "has the right filename and line number in the backtrace" do
        expect(subject.backtrace.first.filename).to eq("foo.rb")
        expect(subject.backtrace.first.line).to eq(123)
      end
    end

    # context "when the exception is an ActionView::Template::Error" do
    #
    #   let(:exception) {
    #     ActionView::Template::Error.new("undefined method `something!' for #<Class:0x00deadbeef>")
    #   }
    #
    #   its(:message) { is_expected.to eq "undefined method `something!' for #<Class:0x00deadbeef>" }
    #
    #   it "has the right filename and line number in the backtrace" do
    #     expect(subject.backtrace.first.filename).to eq("app/views/foo/bar.haml")
    #     expect(subject.backtrace.first.line).to eq(42)
    #   end
    # end
    #
    context "when the exception is a Coffeelint syntax error" do
      before do
        stub_const("Sprockets::Coffeelint::Error", Class.new(SyntaxError))
      end

      let(:exception) {
        Sprockets::Coffeelint::Error.new("[stdin]:11:88: error: unexpected=").tap do |ex|
          ex.set_backtrace(["app/assets/javascripts/files/index.coffee:11", "sprockets/coffeelint.rb:3"])
        end
      }

      its(:message) { is_expected.to eq "[stdin]:11:88: error: unexpected=" }
      its(:type)    { is_expected.to eq Sprockets::Coffeelint::Error }

      it "has the right filename and line number in the backtrace" do
        expect(subject.backtrace.first.filename).to eq("app/assets/javascripts/files/index.coffee")
        expect(subject.backtrace.first.line).to eq(11)
      end
    end

    describe '#hint' do
      subject(:hint) { described_instance.hint }

      it 'uses ExceptionHint to get a hint for the exception' do
        hint
        expect(BetterErrors::ExceptionHint).to have_received(:new).with(exception)
      end

      context "when ExceptionHint returns a string" do
        let(:exception_hint) { instance_double(BetterErrors::ExceptionHint, hint: "Hint text") }

        it 'returns the value from ExceptionHint' do
          expect(hint).to eq("Hint text")
        end
      end
    end
  end
end