sugarcrm/rspec-tabular

View on GitHub
lib/rspec/tabular.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require 'rspec/tabular/version'
require 'rspec/tabular/wrapped'

module RSpec
  # rubocop:disable all

  # This module will allow examples to be specified in a more clean tabular
  # manner.
  #
  # The same tests can be executed using standard lets, context and examples
  # but this will streamline how they are expressed. An example of this is:
  #
  # @example
  # describe 'a thing' do
  #   context { let(:input1) { 'value1' } ; let(:input2) { 'value2' } ; let(:input3) { 'value3' } ; it { should be false } }
  #   context { let(:input1) { 'value4' } ; let(:input2) { 'value5' } ; let(:input3) { 'value6' } ; it { should be true } }
  #   context { let(:input1) { 'value7' } ; let(:input2) { 'value8' } ; let(:input3) { 'value9' } ; it { should eq('foobar') } }
  #   context { let(:input1) { 'value4' } ; let(:input2) { 'value5' } ; let(:input3) { 'value6' } ; its(:method) { should eq('something') } }
  #   context { let(:input1) { 'bad1' }   ; let(:input2) { 'bad2' }   ; let(:input3) { 'bad3' }   ; specify { expect { subject }.to raise_error('error') }
  #   context { let(:input1) { 'bad4' }   ; let(:input2) { 'bad5' }   ; let(:input3) { 'bad6' }   ; specify { expect { subject }.to raise_error(TestException) }
  #   context { let(:input1) { 'value4' } ; let(:input2) { 'value5' } ; let(:input3) { 'value6' } ; specify { helper_method.should eq('expected') } }
  #   context { let(:input1) { 'value4' } ; let(:input2) { 'value5' } ; let(:input3) { 'value6' } ; specify { subject } }
  # end
  #
  # Which works but is not very DRY, and has a great deal of boiler plate code.
  # This module can improve that with the following helper methods:
  #
  # * inputs - defines the list of values for the example
  # * it_with
  # * its_with
  # * raise_error_with
  # * specify_with
  # * side_effects_with - like specify_with but assumes a block of '{ subject }'
  #
  # Limitations:
  # - inputs can only be simple types, and cannot use values defined by let
  #
  # @example
  #   describe 'a thing with explicit blocks' do
  #     subject { subject.thing(input1, input2) }
  #     before ( stub_model(ThingyModel, name: input3) }
  #
  #     inputs(           :input1,  :input2,  :input3)
  #     it_with(          'value1', 'value2', 'value3') { should be false }
  #     it_with(          'value4', 'value5', 'value6') { should be true }
  #     it_with(          'value4', 'value5', 'value6') { should eq('foobar') }
  #
  #     its_with(:method, 'value4', 'value5', 'value6') { should eq('something') }
  #
  #     specify_with(     'value1', 'value2', 'value3' ) { helper_method.should eq('expected') }
  #   end
  #
  #   describe 'a thing with implicit shoulds' do
  #     subject { subject.thing(input1, input2) }
  #     before ( stub_model(ThingyModel, name: input3) }
  #
  #     inputs            :input1,  :input2,  :input3
  #     it_with           'value1', 'value2', 'value3', nil
  #     it_with           'value4', 'value5', 'value6', true
  #     it_with           'value7', 'value8', 'value9', 'foobar'
  #
  #     its_with :method, 'value1', 'value2', 'value3', 'something'
  #
  #     raise_error_with  'bad1',   'bad2',   'bad3',   'error'
  #     raise_error_with  'bad4',   'bad5',   'bad6',   TestException
  #     raise_error_with  'bad4',   'bad5',   'bad6',   TestException, 'error'
  #
  #     side_effects_with 'value4', 'value5', 'value6'
  #   end
  #
  # Values that need an it/before scope can be wrapped using the helper function
  # `with_context`. An optional 2nd parameter will provide a value to write out
  # to the reporter
  #
  #   describe 'a thing with item compairsons' do
  #     subject { subject.thing(input1) }
  #
  #     inputs  :input1
  #     it_with with_context(proc { instance_double(ThingyModel) }), 'foobar'
  #     it_with with_context(proc { double }, 'double'),             'foobar'
  #
  # Inputs can also be specified as block arguments. I am not sure if this is
  # really useful, it might be deprecated in the future.
  #
  # @example
  #   describe 'a thing', :inputs => [:input1, :input2, :input3] do
  #     subject { subject.thing(input1, input2) }
  #     before ( stub_model(ThingyModel, name: input3) }
  #
  #     it_with('value1', 'value2', 'value3') { should be false }
  #   end

  # rubocop:enable all
  module Tabular
    # Example group methods e.g. inside describe/context block
    module ExampleGroup
      def inputs(*args)
        metadata[:inputs] ||= args
      end

      def it_with(*input_values, &block)
        if block.nil? && (metadata[:inputs].size == input_values.size - 1)
          expected_value = input_values.pop
          block = proc { is_expected.to eq(expected_value) }
        end

        context("with #{metadata[:inputs].zip(input_values).to_h}") do
          metadata[:inputs].each_index do |i|
            key = metadata[:inputs][i]
            value = input_values[i]
            let(key) { _unwrap(value) }
          end

          example(nil, { input_values: input_values }, &block)
        end
      end

      alias specify_with it_with

      # Example with an implicit subject execution
      def side_effects_with(*args)
        it_with(*args) do
          subject
        rescue Exception # rubocop:disable Lint/SuppressedException, Lint/RescueException
        end
      end

      def raise_error_with(*args)
        raise_error_args = args
        it_with_args     = raise_error_args.slice!(0, metadata[:inputs].size)

        it_with(*it_with_args) do
          expect { subject }.to raise_error(*raise_error_args)
        end
      end

      def its_with(attribute, *input_values, &block)
        if block.nil? && (metadata[:inputs].size == input_values.size - 1)
          expected_value = input_values.pop
          block = proc { should eq(expected_value) }
        end

        describe("#{attribute} with #{input_values.join(', ')}") do
          if attribute.is_a?(Array)
            let(:__its_subject) { subject[*attribute] }
          else
            let(:__its_subject) do
              attribute_chain = attribute.to_s.split('.')
              attribute_chain.inject(subject) do |inner_subject, attr|
                inner_subject.send(attr)
              end
            end
          end

          def should(matcher = nil, message = nil) # rubocop:disable Lint/NestedMethodDefinition
            RSpec::Expectations::PositiveExpectationHandler.handle_matcher(
              __its_subject, matcher, message
            )
          end

          def should_not(matcher = nil, message = nil) # rubocop:disable Lint/NestedMethodDefinition
            RSpec::Expectations::NegativeExpectationHandler.handle_matcher(
              __its_subject, matcher, message
            )
          end

          metadata[:inputs].each_index do |i|
            key = metadata[:inputs][i]
            value = input_values[i]
            let(key) { _unwrap(value) }
          end

          example(nil, { input_values: input_values }, &block)
        end
      end

      def with_context(proc, pretty_val = nil)
        RSpec::Tabular::Wrapped.new(proc, pretty_val)
      end

      # TODO: it_behaves_like_with(example_name, inputs)
    end

    # example level methods: e.g. inside before/it
    module Example
      def _unwrap(value)
        return value unless value.is_a?(RSpec::Tabular::Wrapped)

        instance_eval(&value.proc)
      end
    end
  end
end

RSpec.configure do |config|
  config.extend(RSpec::Tabular::ExampleGroup)
  config.include(RSpec::Tabular::Example)
end