Skip to content

sleepingkingstudios/rspec-sleeping_king_studios

Repository files navigation

RSpec::SleepingKingStudios

A collection of matchers and extensions to ease TDD/BDD using RSpec. Extends built-in matchers with new functionality, such as support for Ruby 2.0+ keyword arguments, and adds new matchers for testing boolean-ness, object reader/writer properties, object constructor arguments, ActiveModel validations, and more. Also defines shared example groups for more expressive testing.

About

Setup

To add support for RSpec::SleepingKingStudios extensions to rubocop-rspec, add the following to your .rubocop.yml configuration file:

inherit_gem:
  rspec-sleeping_king_studios: 'config/rubocop-rspec.yml'

Compatibility

RSpec::SleepingKingStudios is tested against the following dependencies:

  • Ruby (MRI) 3.1 through 3.3
  • RSpec versions 3.9 through 3.13
  • ActiveModel versions 6.1 through 7.1

Documentation

Documentation is generated using YARD, and can be generated locally using the yard gem.

License

Copyright (c) 2013-2024 Rob Smith

RSpec::SleepingKingStudios is released under the MIT License.

Contribute

The canonical repository for this gem is located at https://github.com/sleepingkingstudios/rspec-sleeping_king_studios.

To report a bug or submit a feature request, please use the Issue Tracker.

To contribute code, please fork the repository, make the desired updates, and then provide a Pull Request. Pull requests must include appropriate tests for consideration, and all code must be properly formatted.

Code of Conduct

Please note that the RSpec::SleepingKingStudios project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.

Configuration

RSpec::SleepingKingStudios now has configuration options available through RSpec.configuration. For example, to set the behavior of the matcher examples when a failure message expectation is undefined (see RSpec Matcher Examples, below), put the following in your spec_helper or other configuration file:

RSpec.configure do |config|
  config.sleeping_king_studios do |config|
    # RSpec::SleepingKingStudios configuration can be set here.
  end # config
end # config

Configuration Options

Examples

Handle Missing Failure Message With
RSpec.configure do |config|
  config.sleeping_king_studios do |config|
    config.examples do |config|
      config.handle_missing_failure_message_with = :ignore
    end # config
  end # config
end # config

This option is used with the RSpec matcher examples (see Examples, below), and determines the behavior when a matcher is expected to fail, but the corresponding failure message is not defined (via let(:failure_message) or let(:failure_message_when_negated)). The default option is :pending, which marks the generated example as skipped (and will show up as pending in the formatter). Other options include :ignore, which marks the generated example as passing, and :exception, which marks the generated example as failing.

Match String Failure Message As
RSpec.configure do |config|
  config.sleeping_king_studios do |config|
    config.examples do |config|
      config.match_string_failure_message_as = :exact
    end # config
  end # config
end # config

This option is used with the RSpec matcher examples (see Examples, below), and determines whether an expected failure message is matched against the actual failure message as an exact match or as a substring. The default option is :substring, which means that any failure message that contains the expected message as a substring will match. Alternatively, setting the option to :exact will mean that only a failure message that is an exact match for the expected message will match.

Matchers

Allow Empty Include Matchers
RSpec.configure do |config|
  config.sleeping_king_studios do |config|
    config.matchers do |config|
      config.allow_empty_include_matchers = false
    end # config
  end # config
end # config

This option is used with the IncludeMatcher (see #include, below). If this option is set to false, an ArgumentError will be raised when attempting to instantiate an IncludeMatcher without any expectations.

This prevents an insidious bug when using the do..end block syntax to create a block expectation while the matcher macro is itself an argument to another function, such as ExpectationTarget#to. This bug causes the block to be silently ignored and any enumerable object to match against the matcher, even an empty object.

Strict Predicate Matching
RSpec.configure do |config|
  config.sleeping_king_studios do |config|
    config.matchers do |config|
      config.strict_predicate_matching = true
    end # config
  end # config
end # config

This option is used with the HavePredicateMatcher (see #have_predicate, below). If set to true, ensures that any method that is expected to be a predicate will return either true or false. The matcher will fail if the method returns any other value. The default value is false, which allows for loose matching of predicate methods.

Concerns

RSpec::SleepingKingStudios defines a few concerns that can be included in or extended into modules or example groups for additional functionality.

Example Constants

require 'rspec/sleeping_king_studios/concerns/example_constants'

RSpec.describe 'constants' do
  extend RSpec::SleepingKingStudios::Concerns::ExampleConstants

  example_constant 'THE_ANSWER', 42

  example_class 'Spec::Examples::Question' do |klass|
    value = help_string

    klass.send(:define_method, :answer) { THE_ANSWER }
    klass.send(:define_method, :help)   { value }
  end # example_class

  let(:described_class) { Spec::Examples::Question }
  let(:instance)        { described_class.new }
  let(:help_string) do
    "It looks like you're defining a class. Would you like help?"
  end # let

  it { expect(described_class.name).to be == 'Spec::Examples::Question' }

  it { expect(instance.answer).to be THE_ANSWER }

  it { expect(instance.help).to be == help_string }
end # describe

Provides a programmatic way to define temporary constants and classes scoped to the current example.

::example_constant

param constant_name [String, Symbol] The name of the constant. Can be a qualified name separated by :: (e.g. 'Spec::Examples::Question'), in which case any missing modules will be temporarily set as well.

param constant_value Defaults to nil. If the constant value is not set and a block is given, the block will be executed in the context of the example (so previously-set constants will be available, as well as example features such as the values of let blocks) and the value of the constant will be set to the result of the block call.

option force [Boolean] Defaults to false. If the constant is already defined, trying to set the constant value will raise an error unless the force option is set to true.

Sets the value of the named constant to the specified value within the context of the current example.

::example_class

param constant_name [String, Symbol] The name of the constant. Can be a qualified name separated by :: (e.g. 'Spec::Examples::Question'), in which case any missing modules will be temporarily set as well.

option base_class [Class] Defaults to Object. The base class of the generated class.

yield klass [Class] If a block is given, it is executed in the context of the example (so previously-set constants will be available, as well as example features such as the values of let blocks) and yielded the class.

Creates a new class with the specified base class and sets the value of the named constant to the created class within the context of the current example.

Include Contract

require 'rspec/sleepingkingstudios/concerns/include_contract'

Defines helpers for including reusable contracts in RSpec example groups.

Contracts are a mechanism for sharing tests between projects. For example, one library may define an interface or specification for a type of object, while a second library implements that object. By defining a contract and sharing that contract as part of the library, the developer ensures that any object that matches the contract has correctly implemented and conforms to the interface. This reduces duplication of tests and provides resiliency as an interface is developed over time and across versions of the library.

Mechanically speaking, each contract encapsulates a section of RSpec code. When the contract is included in a spec, that code is then injected into the spec. Writing a contract, therefore, is no different than writing any other RSpec specification - it is only the delivery mechanism that differs. A contract can be any object that responds to #to_proc; the simplest contract is therefore a Proc or lambda that contains some RSpec code.

Although the examples use bare lambdas, any object that responds to #to_proc can be used as a contract. See also Contracts, which provide an approach to defining contracts that makes documenting contracts much cleaner.

module ExampleContracts
  # This contract asserts that the object has the Enumerable module as an
  # ancestor, and that it responds to the #each method.
  SHOULD_BE_ENUMERABLE_CONTRACT = lambda do
    it 'should be Enumerable' do
      expect(subject).to be_a Enumerable
    end

    it 'should respond to #each' do
      expect(subject).to respond_to(:each).with(0).arguments
    end
  end
end

RSpec.describe Array do
  extend RSpec::SleepingKingStudios::Concerns::IncludeContract

  include_contract ExampleContracts::SHOULD_BE_ENUMERABLE_CONTRACT
end

RSpec.describe Hash do
  extend  RSpec::SleepingKingStudios::Concerns::IncludeContract
  include ExampleContracts

  include_contract 'should be enumerable'
end

We can also write contracts that take parameters, allowing us to customize the expected behavior of the object.

module SerializerContracts
  # This contract asserts that the serialized result has the expected values.
  SHOULD_SERIALIZE_ATTRIBUTES_CONTRACT = lambda \
  do |*attributes, **values, &block|
    describe '#serialize' do
      let(:serialized) { subject.serialize }

      it { expect(subject).to respond_to(:serialize).with(0).arguments }

      attributes.each do |attribute|
        it "should serialize #{attribute}" do
          expect(serialized[attribute]).to be == subject[attribute]
        end
      end

      values.each do |attribute, value|
        it "should serialize #{attribute}" do
          expect(serialized[attribute]).to be == value
        end
      end

      instance_exec(&block) if block
    end
  end
end

RSpec.describe CaptainPicard do
  extend  RSpec::SleepingKingStudios::Concerns::IncludeContract
  include SerializerContracts

  include_contract 'should serialize attributes',
    :name,
    :rank,
    lights: 4 do
      it 'should serialize the catchphrase' do
        expect(serialized[:catchphrase]).to be == 'Make it so.'
      end
  end
end

First, we pass the contract a series of attribute names. These are used to assert that the serialized attributes match the values on the original object.

Second, we pass the contract a set of attribute names and values. These are used to assert that the serialized attributes have the specified values.

Finally, we can pass the contract a block, which the contract then executes. Note that the block is executed in the context of our describe block, and thus can take advantage of our memoized #serialized helper method.

.include_contract

The .include_contract class method applies the contract to the current example group. It passes any additional arguments, keywords, or block to the contract implementation.

.finclude_contract

The .finclude_contract class method creates a new focused context inside the current example group and applies the contract to that context. If RSpec is configured to run only focused specs, then only the contract specs will be run. This is useful to quickly focus and run the specs from a particular contract.

.xinclude_contract

The .xinclude_contract class method creates a new skipped context inside the current example group and applies the contract to that context. The contract specs will not be run, but will instead be marked as pending. This is useful to temporarily disable the specs from a particular contract.

Focus Examples

require 'rspec/sleeping_king_studios/concerns/focus_examples'

RSpec.describe String do
  extend RSpec::SleepingKingStudios::Concerns::FocusExamples

  shared_examples 'should be a greeting' do
    it { expect(salutation).to be =~ /greeting/i }
  end # shared_examples

  shared_examples 'should greet the user by name' do
    it { expect(salutation).to match user.name }
  end # shared_examples

  let(:salutation) { 'Greetings, programs!' }

  # Focused example groups are always run when config.filter_run :focus is
  # set to true.
  finclude_examples 'should be a greeting'

  # Skipped example groups are marked as pending and never run.
  xinclude_examples 'should greet the user by name'
end # describe

A shorthand for focusing or skipping included shared example groups with a single keystroke, e.g. include_examples '...' => finclude_examples '...'.

A simplified syntax for re-using shared context or examples without having to explicitly wrap them in describe blocks or allowing memoized values or callbacks to change the containing context. In the example above, if the programmer had used the standard include_context instead, the first expectation would have failed, as the value of :quote would have been overwritten.

Important Note: Do not use these methods with example groups that have side effects, e.g. that define a memoized #let helper or a #before block that is intended to modify the behavior of sibling examples. Wrapping the example group in a describe block breaks that relationship. Best practice is to use the #wrap_examples method to safely encapsulate example groups with side effects, and the #fwrap_examples method to automatically focus such groups.

::finclude_examples

(also ::finclude_context) A shortcut for focusing the example group by wrapping it in a describe block, similar to the built-in fit and fdescribe methods.

::xinclude_examples

(also ::xinclude_context) A shortcut for skipping the example group by wrapping it in a describe block, similar to the built-in xit and xdescribe methods.

Memoized Helpers

require 'rspec/sleepingkingstudios/concerns/memoized_helpers'

Methods for defining memoized helpers, similar to the core RSpec let method.

.let?

The .let? class method defines a memoized helper if and only if there is not an existing method with the same name defined in the example group. Among other use cases, this allows for defining default values in shared examples.

RSpec.describe Rocket do
  subject(:rocket) { described_class.new(name: 'Imp IV') }

  shared_examples 'should launch the rocket' do
    let?(:launch_site) { 'KSC' }

    it 'should launch the rocket' do
      expect { rocket.launch_from(launch_site) }
        .to change(rocket, :launched?)
        .to be true
    end
  end

  describe '#launch_from' do
    include_examples 'should launch the rocket'

    describe 'with launch_site: value' do
      let(:launch_site) { 'Baikerbanur' }

      include_examples 'should launch the rocket'
    end
  end
end

Shared Example Groups

require 'rspec/sleeping_king_studios/concerns/shared_example_group'

module MyCustomExamples
  extend RSpec::SleepingKingStudios::Concerns::SharedExampleGroup

  shared_examples 'has custom behavior' do
    # Define expectations here...
  end # shared_examples
  alias_shared_examples 'should have custom behavior', 'has custom behavior'
end # module

Utility functions for defining shared examples. If included in a module, any shared examples defined in that module are scoped to the module, rather than placed in a global scope. This allows you to define different shared examples with the same name in different contexts, similar to the current behavior when defining a shared example inside an example group. To use the defined examples, simply include the module in an example group. Important Note: Shared examples and aliases must be defined before including the module in an example group. Any shared examples or aliases defined afterword will not be available inside the example group.

::alias_shared_examples

(also ::alias_shared_context) Aliases a defined shared example group, allowing it to be accessed using a new name. The example group must be defined in the current context using shared_examples. The aliases must be defined before including the module into an example group, or they will not be available in the example group.

::shared_examples

(also ::shared_context) Defines a shared example group within the context of the current module. Unlike a top-level example group defined using RSpec#shared_examples, these examples are not globally available, and must be mixed into an example group by including the module. The shared examples must be defined before including the module, or they will not be available in the example group.

Toolbelt

require 'rspec/sleeping_king_studios/concerns/toolbelt'

RSpec.describe "a String" do
  include RSpec::SleepingKingStudios::Concerns::Toolbelt

  shared_examples 'should process' do |string|
    singular = tools.string.singularize(string)
    plural   = tools.string.pluralize(string)

    it "should singularize #{string} to #{singular}" do
      expect(tools.singularize string).to be_a String
    end # it

    it "should pluralize #{string} to #{plural}" do
      expect(tools.pluralize string).to be_a String
    end # it
  end # shared_examples

  include_examples 'should pluralize', 'light'
end # describe

A helper module for exposing SleepingKingStudios::Tools methods in examples and example groups.

Wrap Environment

require 'rspec/sleeping_king_studios/concerns/wrap_env'

RSpec.describe 'environment' do
  include RSpec::SleepingKingStudios::Concerns::WrapEnv

  it { expect(ENV['VAR_NAME']).to be nil }

  context 'when the variable is set in the example group' do
    wrap_env 'VAR_NAME', 'custom_value'

    it { expect(ENV['VAR_NAME']).to be == 'custom_value' }
  end # context

  context 'when the variable is set in the example group with a block' do
    let(:calculated_value) { 'calculated_value' }

    wrap_env('VAR_NAME') { calculated_value }

    it { expect(ENV['VAR_NAME']).to be == calculated_value }
  end # context

  context 'when the variable is set inside an example' do
    it 'should set the variable' do
      expect(ENV['VAR_NAME']).to be nil

      begin
        wrap_env('VAR_NAME', 'new_value') do
          expect(ENV['VAR_NAME']).to be == 'new_value'

          raise RuntimeError, 'must handle errors and reset the var'
        end # wrap_env
      rescue RuntimeError
      end # begin-rescue

      expect(ENV['VAR_NAME']).to be nil
    end # it
  end # context
end # describe

Provides helper methods for temporarily overwriting values in the environment, which are safely and automatically reset after the example or block.

Wrap Examples

require 'rspec/sleeping_king_studios/concerns/wrap_examples'

RSpec.describe String do
  extend RSpec::SleepingKingStudios::Concerns::WrapExamples

  shared_context 'with a long quote' do
    let(:quote) do
      'Greetings, starfighter! You have been recruited by the Star League'\
      ' to defend the frontier against Xur and the Ko-Dan armada!'
    end # let
  end # shared context

  shared_context 'with a short quote' do`
    let(:quote) { 'Greetings, programs!' }
  end # shared_context

  describe '#length' do
    wrap_context 'with a long quote' do
      it { expect(quote.length).to be == 124 }
    end # wrap_context

    wrap_context 'with a short quote' do
      it { expect(quote.length).to be == 20 }
    end # wrap_context
  end # describe
end # describe

A simplified syntax for re-using shared context or examples without having to explicitly wrap them in describe blocks or allowing memoized values or callbacks to change the containing context. In the example above, if the programmer had used the standard include_context instead, the first expectation would have failed, as the value of :quote would have been overwritten.

::wrap_examples

(also ::wrap_context) Creates an implicit describe block and includes the context or examples within the describe block to avoid leaking values or callbacks to the outer context. Any parameters or keywords will be passed along to the include_examples call. If a block is given, it is evaluated in the context of the describe block after the include_examples call, allowing you to define additional examples or customize the values and callbacks defined in the shared examples.

::fwrap_examples

(also ::fwrap_context) A shortcut for wrapping the context or examples in an automatically-focused describe block, similar to the built-in fit and fdescribe methods.

::xwrap_examples

(also ::xwrap_context) A shortcut for wrapping the context or examples in an automatically-skipped describe block, similar to the built-in xit and xdescribe methods.

Contracts

require 'rspec/sleeping_king_studios/contract'

An RSpec::SleepingKingStudios::Contract object encapsulates a partial RSpec specification. Unlike a traditional shared example group, a contract can be reused across projects, allowing a library to define an interface, provide a reference implementation, and publish tests that validate other implementations.

Contracts can be added to an example group either through the .apply method or using the Include Contract concern.

module ExampleContracts
  # This contract asserts that the object has the Enumerable module as an
  # ancestor, and that it responds to the #each method.
  class ShouldBeEnumerableContract
    extend RSpec::SleepingKingStudios::Contract

    # @!method apply(example_group)
    #   Adds the contract to the example group.

    contract do
      it 'should be Enumerable' do
        expect(subject).to be_a Enumerable
      end

      it 'should respond to #each' do
        expect(subject).to respond_to(:each).with(0).arguments
      end
    end
  end
end

RSpec.describe Array do
  ExampleContracts::SHOULD_BE_ENUMERABLE_CONTRACT.apply(self)
end

RSpec.describe Hash do
  extend  RSpec::SleepingKingStudios::Concerns::IncludeContract
  include ExampleContracts

  include_contract 'should be enumerable'
end

The major advantage a Contract object provides over using a Proc is documentation - tools such as YARD do not gracefully handle bare lambdas, while the functionality and requirements of a Contract can be specified using standard patterns, such as documenting the parameters passed to a contract using the #apply method.

Contracts can be defined with parameters, which allows us to customize the expected behavior of the object.

module SerializerContracts
  # This contract asserts that the serialized result has the expected
  # values.
  #
  # First, we pass the contract a series of attribute names. These are
  # used to assert that the serialized attributes match the values on the
  # original object.
  #
  # Second, we pass the contract a set of attribute names and values.
  # These are used to assert that the serialized attributes have the
  # specified values.
  #
  # Finally, we can pass the contract a block, which the contract then
  # executes. Note that the block is executed in the context of our
  # describe block, and thus can take advantage of our memoized
  # #serialized helper method.
  class ShouldSerializeAttributesContract
    extend RSpec::SleepingKingStudios::Contract

    contract do |*attributes, **values, &block|
      describe '#serialize' do
        let(:serialized) { subject.serialize }

        it { expect(subject).to respond_to(:serialize).with(0).arguments }

        attributes.each do |attribute|
          it "should serialize #{attribute}" do
            expect(serialized[attribute]).to be == subject[attribute]
          end
        end

        values.each do |attribute, value|
          it "should serialize #{attribute}" do
            expect(serialized[attribute]).to be == value
          end
        end

        instance_exec(&block) if block
      end
    end
  end

RSpec.describe CaptainPicard do
  SerializerContracts::ShouldSerializeAttributesContract.apply(
    self,
    :name,
    :rank,
    lights: 4) \
  do
    it 'should serialize the catchphrase' do
      expect(serialized[:catchphrase]).to be == 'Make it so.'
    end
  end
end

Contract Methods

Each RSpec::SleepingKingStudios::Contract defines the following methods.

.apply

The .apply method adds the contract to the given example group, using the same internals used in the Include Contract concern.

RSpec.describe Book do
  subject(:book) { Book.first }

  SerializerContracts::ShouldSerializeAttributesContract.apply(
    self,
    :title,
    author: book.author.as_json
  )
end

.contract

The .contract method is used to define the contract implementation.

module ModelContracts
  class ShouldHavePrimaryKey
    extend RSpec::SleepingKingStudios::Contract

    contract do |primary_key_name: :id|
      describe "##{primary_key_name}" do
        it { expect(subject).to respond_to(primary_key_name).with(0).arguments }
      end
    end
  end
end

If a block is not given, it returns the current implementation as a Proc (or nil, if a contract implementation has not yet been set).

.to_proc

The .to_proc method returns the current implementation as a Proc (or nil, if a contract implementation has not yet been set).

Matchers

To enable a custom matcher, simply require the associated file. Matchers can be required individually or by category:

require 'rspec/sleeping_king_studios/all'
#=> requires all features, including matchers

require 'rspec/sleeping_king_studios/matchers/core/all'
#=> requires all of the core matchers

require 'rspec/sleeping_king_studios/matchers/core/construct'
#=> requires only the :construct matcher

As of version 2.2, you can also load only the matcher, without adding the associated macro to your example groups. This can be useful in case of naming conflicts with other libraries, or if you need only the matcher in isolation.

require 'rspec/sleeping_king_studios/matchers/core/be_boolean_matcher'
#=> requires the matcher itself as RSpec::SleepingKingStudios::Matchers::Core::BeBooleanMatcher,
#   but does not add a #be_boolean macro to example groups.

ActiveModel

require 'rspec/sleeping_king_studios/matchers/active_model/all'

These matchers validate ActiveModel functionality, such as validations.

#have_errors Matcher

require 'rspec/sleeping_king_studios/matchers/active_model/have_errors'

Verifies that the actual object has validation errors. Optionally can specify individual fields to validate, or even specific messages for each attribute.

How To Use:

expect(instance).to have_errors

expect(instance).to have_errors.on(:name)

expect(instance).to have_errors.on(:name).with_message('not to be nil')

Chaining:

  • #on: [String, Symbol] Adds a field to validate; the matcher only passes if all validated fields have errors.
  • #with: [Array] Adds one or more messages to the previously-defined field validation. Raises ArgumentError if no field was previously set.
  • #with_message: [String] Adds a message to the previously-defined field validation. Raises ArgumentError if no field was previously set.
  • #with_messages: [Array] Adds one or more messages to the previously-defined field validation. Raises ArgumentError if no field was previously set.

BuiltIn

require 'rspec/sleeping_king_studios/matchers/built_in/all'

These extend the built-in RSpec matchers with additional functionality.

#be_kind_of Matcher

require 'rspec/sleeping_king_studios/matchers/built_in/be_kind_of'

Now accepts an Array of types. The matcher passes if the actual object is any of the parameter types.

Also allows nil parameter as a shortcut for NilClass.

How To Use:

expect(instance).to be_kind_of [String, Symbol, nil]
#=> passes iff instance is a String, a Symbol, or is nil

#include Matcher

require 'rspec/sleeping_king_studios/matchers/built_in/include'

Now accepts Proc parameters; items in the actual object are passed into proc#call, with a truthy response considered a match to the item. In addition, now accepts an optional block as a shortcut for adding a proc expectation.

How To Use:

expect(instance).to include { |item| item =~ /pattern/ }

#respond_to Matcher

require 'rspec/sleeping_king_studios/matchers/built_in/respond_to'

Now has additional chaining functionality to validate the number of arguments accepted by the method, the keyword arguments (if any) accepted by the method, and whether the method accepts a block argument.

How To Use:

# With a block.
expect(instance).to respond_to(:foo).with_a_block.

# With a variable number of arguments and a block.
expect(instance).to respond_to(:foo).with(2..3).arguments.and_a_block

# With keyword arguments.
expect(instance).to respond_to(:foo).with_keywords(:bar, :baz)

# With both arguments and keywords.
expect(instance).to respond_to(:foo).with(2).arguments.and_keywords(:bar, :baz)

Chaining:

  • #with: Expects at most one Integer or Range argument, and zero or more Symbol arguments corresponding to optional keywords. Verifies that the method accepts that keyword, or has a variadic keyword of the form **kwargs. As of 2.1.0 and required keywords, verifies that all required keywords are provided.
  • #with_unlimited_arguments: (also and_unlimited_arguments) No parameters. Verifies that the method accepts any number of arguments via a variadic argument of the form *args.
  • #with_a_block: (also and_a_block) No parameters. Verifies that the method requires a block argument of the form &my_argument. Important note: A negative result does not mean the method cannot accept a block, merely that it does not require one. Also, does not check whether the block is called or yielded.
  • #with_keywords: (also and_keywords) Expects one or more String or Symbol arguments. Verifies that the method accepts each provided keyword or has a variadic keyword of the form **kwargs. As of 2.1.0 and required keywords, verifies that all required keywords are provided.
  • #with_any_keywords: (also and_any_keywords, and_arbitrary_keywords, and_arbitrary_keywords) No parameters. Verifies that the method accepts any keyword arguments via a variadic keyword of the form **kwargs.

Core

require 'rspec/sleeping_king_studios/matchers/core/all'

These matchers check core functionality, such as object boolean-ness, the existence of properties, and so on.

#be_a_uuid Matcher

require 'rspec/sleeping_king_studios/matchers/core/be_a_uuid'

Aliases: #a_uuid.

How To Use:

# With an object comparison.
expect(string).to be_a_uuid

# Inside a composable matcher.
expect(array).to include(a_uuid)

Parameters: None.

#be_boolean Matcher

require 'rspec/sleeping_king_studios/matchers/core/be_boolean'

Checks if the provided object is true or false.

Aliases: #a_boolean.

How To Use:

# With an object comparison.
expect(object).to be_boolean

# Inside a composable matcher.
expect(array).to include(a_boolean)

Parameters: None.

#construct Matcher

require 'rspec/sleeping_king_studios/matchers/core/construct'

Verifies that the actual object can be constructed using ::new. Can take an optional number of arguments.

How To Use:

# With an expected number of arguments.
expect(described_class).to construct.with(1).arguments

# With an expected number of arguments and set of keywords.
expect(instance).to construct.with(0, :bar, :baz)

Parameters: None.

Chaining:

  • #with: Expects one Integer, Range, or nil argument, and zero or more Symbol arguments corresponding to optional keywords. Verifies that the class's constructor accepts that keyword, or has a variadic keyword of the form **params. As of Ruby 2.1 and required keywords, verifies that all required keywords are provided.
  • #with_unlimited_arguments: (also and_unlimited_arguments) No parameters. Verifies that the class's constructor accepts any number of arguments via a variadic argument of the form *args.
  • #with_keywords: (also and_keywords) Expects one or more String or Symbol arguments. Verifies that the class's constructor accepts each provided keyword or has a variadic keyword of the form **params. As of 2.1.0 and required keywords, verifies that all required keywords are provided.
  • #with_arbitrary_keywords: (also and_arbitrary_keywords) No parameters. Verifies that the class's constructor accepts any keyword arguments via a variadic keyword of the form **params.

#deep_match Matcher

require 'rspec/sleeping_king_studios/matchers/core/deep_match'

Performs a recursive comparison between two object or data structures. Also supports using RSpec matchers as values in the expected object.

How To Use:

expected = {
  status: 200,
  body: {
    order: {
      id:    an_instance_of(Integer),
      total: '9.99'
    }
  }
}
expect(response).to deep_match(expected)

When the value does not match the expectation, the failure message will provide a detailed diff showing added, missing, and changed values.

response = {
  status: 400,
  body: {
    order: {
      fulfilled: false,
      total:     '19.99'
    }
  },
  errors: ['Insufficient funds']
}
expect(response).to deep_match(expected)
# Failure/Error: expect(response).to deep_match(expected)
#
#   expected: == {:body=>{:order=>{:id=>an instance of Integer, :total=>"9.99"}}, :status=>200}
#        got:    {:status=>400, :body=>{:order=>{:fulfilled=>false, :total=>"19.99"}}, :errors=>["Insufficient funds"]}
#
#   (compared using Hashdiff)
#
#   Diff:
#   + :body.:order.:fulfilled => got false
#   - :body.:order.:id => expected an instance of Integer
#   ~ :body.:order.:total => expected "9.99", got "19.99"
#   + :errors => got ["Insufficient funds"]
#   ~ :status => expected 200, got 400

#have_aliased_method Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_aliased_method'

Checks if the object aliases the specified method with the specified other name. Matches if and only if the object responds to both the old and new method names, and if the old method and the new method are the same method.

How To Use:

expect(object).to have_aliased_method(:old_method).as(:new_method)

Parameters: Old method name. Expects the name of the method which has been aliased as a String or Symbol.

Chaining:

  • #as: Required. Expects one String or Symbol, which is the name of the generated method.

#have_changed Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_changed'

Checks that a watched value has changed. The have_changed matcher must be paired with a value spy (see #watch_value, below), which is typically created with the watch_value helper. This is an alternative to the core RSpec change matcher, but allows you track changes to multiple values without nested expectations or other workarounds.

How To Use

spy = watch_value(object, property)

object.property = 'new value'

expect(spy).to have_changed

You can also create a value spy with a block:

spy = watch_value { object.property }

Parameters: None.

Chaining:

  • by: Expects one argument. If the value has changed, then the current value will be subtracted from the initial value and the difference compared with the expected value given to #by. Note: not_to have_changed.by() is not supported and will raise an error.
  • from: Expects one argument. The initial value of the spy (at the time the spy was initialized) must be equal to the given value.
  • to: Expects one argument. The current value of the spy (at the time expect().to have_changed is evaluated) must be equal to the given value.

Warning: Make sure that the value spy is initialized before running whatever code is expected to change the value. In particular, if you are setting up your spies using RSpec let() statements, it is recommended to use the imperative let!() form to ensure that the spies are initialized before running the example.

#have_constant Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_constant'

Checks for the presence of a defined constant :CONSTANT_NAME and optionally the value of the constant. Can also check that the value is immutable, e.g. for an additional layer of protection over important constants.

How To Use:

expect(instance).to have_constant(:FOO)

expect(instance).to have_constant(:BAR).with_value('Bar')

expect(instance).to have_immutable_constant(:BAZ).with_value('Baz')

Parameters: Constant name. Expects a string or symbol that is a valid identifier.

Chaining:

  • #immutable: Sets a mutability expectation, which passes if the value of the constant is immutable. Values of nil, false, true are always immutable, as are Numeric and Symbol primitives. Array values must be frozen and all array items must be immutable. Hash values must be frozen and all hash keys and values must be immutable. All other objects must be frozen.

  • #with: (also #with_value) Expects true or false, which is checked against the current value of actual.property?() if actual responds to #property?.

#have_predicate Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_predicate'

Checks if the actual object responds to #property?, and optionally if the current value of actual.property?() is equal to a specified value. If config.sleeping_king_studios.matchers.strict_predicate_matching is set to true, will also validate that the #property? returns either true or false.

How To Use:

expect(instance).to have_predicate(:foo?).with(true)

Parameters: Property. Expects a string or symbol that is a valid identifier.

Chaining:

  • #with: (also #with_value) Expects true or false, which is checked against the current value of actual.property?() if actual responds to #property?.

#have_property Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_property'

Checks if the actual object responds to #property and #property=, and optionally if the current value of actual.property() is equal to a specified value.

How To Use:

expect(instance).to have_property(:foo).with("foo")

expect(instance).to have_property(:foo, :allow_private => true).with("foo")

Parameters:

param property [String, Symbol] The name of the property.

option allow_private [Boolean] Defaults to false. If true, the matcher will also match a private or protected reader or writer method.

Chaining:

  • #with: (also #with_value) Expects one object, which is checked against the current value of actual.property() if actual responds to #property. Can also be used with an RSpec matcher:

    expect(instance).to have_property(:bar).with(an_instance_of(String))

#have_reader Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_reader'

Checks if the actual object responds to #property with 0 arguments, and optionally if the current value of actual.property() is equal to a specified value.

How To Use:

expect(instance).to have_reader(:foo).with("foo")

expect(instance).to have_reader(:foo, :allow_private => true).with("foo")

Parameters:

param property [String, Symbol] The name of the reader method.

option allow_private [Boolean] Defaults to false. If true, the matcher will also match a private or protected method.

Chaining:

  • #with: (also #with_value) Expects one object, which is checked against the current value of actual.property() if actual responds to #property. Can also be used with an RSpec matcher: expect(instance).to have_reader(:bar).with(an_instance_of(String))

#have_writer Matcher

require 'rspec/sleeping_king_studios/matchers/core/have_writer'

Checks if the actual object responds to #property=.

How To Use:

expect(instance).to have_writer(:foo=)

expect(instance).to have_writer(:foo=, :allow_private => true)

Parameters:

param property [String, Symbol] The name of the writer method. An equals sign '=' is automatically added if the identifier does not already terminate in '='.

option allow_private [Boolean] Defaults to false. If true, the matcher will also match a private or protected method.

#watch_value Helper

require 'rspec/sleeping_king_studios/matchers/core/have_changed'

Creates a value spy that watches the value of a method call or block. The spy also caches the initial value at the time the spy was created; this allows comparisons between the initial and current values. Value spies are used with the #have_changed matcher (see above).

How To Use:

spy = watch_value(object, property)

spy = watch_value { object.property }

Parameters:

param object [Object] The object to watch. Ignored if given a block.

param method_name [String, Symbol] The name of the method to watch. Ignored if given a block.

Chaining: None.

Sandbox

The RSpec::SleepingKingStudios::Sandbox module allows for running a spec file or files in an isolated environment and capturing the results. This can be useful for testing code meant to enhance your tests, such as a custom RSpec matcher or a shared example group.

First, define a spec file to run. As a recommended convention, spec files to be run in a sandbox should be given the spec.fixture.rb suffix to ensure they are not accidentally run with the main test suite.

# frozen_string_literal: true

# In spec/rocket_spec.fixture.rb:
RSpec.describe Rocket do
  describe '#launch' do
    it 'should launch the rocket' do
      expect { subject.launch }.to change(subject, :launched?).to be true
    end
  end
end

Defining fixture files rather than generating temporary files is recommended for performance reasons, but both approaches are possible. Once the file is defined, it can be run in a sandbox:

result = RSpec::SleepingKingStudios::Sandbox.run('spec/rocket_spec.fixture.rb')

result.class #=> RSpec::SleepingKingStudios::Sandbox::Result
result.output #=> """
# Rocket
#   #launch
#     should launch the rocket
#
# 1 example, 0 failures
# """
result.status #=> 1
result.summary #=> 1 example, 0 failures
result.example_descriptions #=> [
#   'Rocket#launch should launch the rocket'
# ]

The .run method returns an instance of RSpec::SleepingKingStudios::Sandbox::Result, which wraps the result of running the specified spec files. It defines the following methods:

  • #errors: The output captured from STDERR when running the files.
  • #example_descriptions: The full description for each evaluated example.
  • #json: The json output from running the files.
  • #output: The output captured from STDOUT when running the files. The specs are run with the --format=doc flag, so this will include the individual examples as well as the summary.
  • #summary: The summary line for the tests.

Deferred Examples

RSpec::SleepingKingStudios::Deferred provides a mechanism for defining specifications that can be reused and shared between projects. For example, a library could use deferred examples to define an interface and test a reference implementation; projects that use that library could then use the published deferred examples to validate their own implementations of that interface.

At its core, a deferred example group is a Ruby module, and hooks into the inheritance hierarchy when included into an example group. This gives certain advantages relative to traditional shared examples (and contracts):

  • Deferred examples are defined in a specific context, and can be shared by simply include-ing the containing module (or the deferred examples directly). There is no global namespace, and using deferred examples (even from other projects) is as simple as an include call.
  • Deferred examples stack, rather than conflict. This means helper methods and memoized helpers can reference other included examples using super(), rather than simply overwriting other helpers at the same "level".
  • Unlike shared example groups, deferred examples defined via a Provider (see Parameterized Examples, below) can accept a block parameter.
module ShouldBeAVehicleExamples
  include RSpec::SleepingKingStudios::Deferred::Examples

  describe '#type' do
    it { expect(subject).to respond_to(:type).with(0).arguments }

    it { expect(subject.type).to be == expected_type }
  end
end

RSpec.describe Rocket do
  include ShouldBeAVehicleExamples

  subject(:rocket) { described_class.new }

  let(:expected_type) { :rocket }

  describe '#launch' do
    it { expect(rocket).to respond_to(:launch) }
  end
end

Parameterized Examples

Deferred examples can also be defined with parameters using the Deferred::Provider DSL. Defining a parameterized example group allows for defining and sharing specs that describe complex and conditional behavior.

For deferred specs that set up a context rather than examples, deferred_context is provided as an alias to Provider.deferred_examples.

module VehicleExamples
  include RSpec::SleepingKingStudios::Deferred::Provider

  deferred_context 'when the vehicle needs to be serviced' \
  do |serviced_at: '2020-01-01'|
    before(:example) do
      vehicle.last_serviced_at = serviced_at
    end
  end

  deferred_examples 'should be a Vehicle' do |vehicle_type:|
    it { expect(subject).to be_a Spec::Models::Vehicle }

    describe '#type' do
      it { expect(subject.type).to be == vehicle_type }
    end
  end
end

The deferred examples can be included in example groups using the Deferred::Consumer DSL.

RSpec.describe Car do
  include RSpec::SleepingKingStudios::Deferred::Consumer
  include VehicleExamples

  subject(:car) { described_class.new }

  include_deferred 'should be a Vehicle', vehicle_type: :car
end

RSpec.describe Rocket do
  include RSpec::SleepingKingStudios::Deferred::Consumer
  include VehicleExamples

  subject(:rocket) { described_class.new }

  include_deferred 'should be a Vehicle', vehicle_type: :rocket
end

Deferred::Consumer also defines support for wrapped deferred examples, which automatically generate a new context and include the deferred examples in the new example group. If #wrap_deferred is passed a block, that block will automatically be evaluated in the context of the example group, allowing you to define additional context or examples.

RSpec.describe Car do
  include RSpec::SleepingKingStudios::Deferred::Consumer
  include VehicleExamples

  subject(:car) { described_class.new }

  wrap_deferred 'when the vehicle needs to be serviced' do
    it { expect(car.last_serviced_at).to be == '2020-01-01' }
  end
end

Deferred::Consumer also includes the shortcuts #finclude_deferred and #fwrap_deferred to automatically focus deferred examples, or #xinclude_deferred and #xwrap_deferred to skip deferred examples.

The defined_deferred_examples? and defined_deferred_context? methods allow checking for the presence of a deferred example group.

RSpec.describe Boat do
  include RSpec::SleepingKingStudios::Deferred::Consumer
  include VehicleExamples

  subject(:boat) { described_class.new }

  if defined_deferred_examples?('should be a water Vehicle')
    include_deferred 'should be a water Vehicle'
  else
    pending 'deferred examples "should be a water Vehicle" is not defined'
  end
end

Shared Examples

To use a custom example group, require the associated file and then include the module in your example group:

require 'rspec/sleeping_king_studios/examples/some_examples'

RSpec.describe MyCustomMatcher do
  include RSpec::SleepingKingStudios::Examples::SomeExamples

  # You can use the custom shared examples here.
  include_examples 'some examples'
end # describe

Unless otherwise noted, these shared examples expect the example group to define either an explicit #instance method (using let(:instance) {}) or an implicit subject. Their behavior is undefined if neither #instance nor subject is defined.

Property Examples

These examples are shorthand for defining a property expectation.

require 'rspec/sleeping_king_studios/examples/property_examples'

RSpec.describe MyClass do
  include RSpec::SleepingKingStudios::Examples::PropertyExamples

  # You can use the custom shared examples here.
end # describe

Should Have Class Property

include_examples 'should have class property', :foo, 42

Delegates to the #have_property matcher (see Core/#have_property, above) and passes if described_class responds to the specified reader and writer methods. If a value is specified, the described class must respond to the property and return the specified value. Alternatively, you can set a proc as the expected value, which can contain a comparison, an RSpec expectation, or a more complex expression:

include_examples 'should have class property', :bar, ->() { an_instance_of(String) }

include_examples 'should have class property', :baz, ->(value) { value.count = 3 }

Should Have Class Reader

include_examples 'should have class reader', :foo, 42

Delegates to the #have_reader matcher (see Core/#have_reader, above) and passes if described_class responds to the specified property reader. If a value is specified, the described class must respond to the property and return the specified value. Alternatively, you can set a proc as the expected value, which can contain a comparison, an RSpec expectation, or a more complex expression:

include_examples 'should have class reader', :bar, ->() { an_instance_of(String) }

include_examples 'should have class reader', :baz, ->(value) { value.count = 3 }

Should Have Class Writer

include_examples 'should have class writer', :foo=

Delegates to the #have_writer matcher (see Core/#have_writer, above) and passes if described_class responds to the specified property writer.

Should Have Constant

include_examples 'should have constant', :FOO, 42

Delegates to the #have_constant matcher (see Core/#have_constant, above) and passes if the described class defines the specified constant. If a value is specified, the class or module must define the constant with the specified value. Alternatively, you can set a proc as the expected value, which can contain a comparison, an RSpec expectation, or a more complex expression:

include_examples 'should have constant', :BAR, ->() { an_instance_of(String) }

include_examples 'should have constant', :BAZ, ->(value) { value.count = 3 }

Should Have Immutable Constant

include_examples 'should have immutable constant', :FOO, 42

As the 'should have constant' example, but sets a mutability expectation on the constant. See Core/#have_constant for specifics on which objects are considered mutable.

Should Have Predicate

include_examples 'should have predicate', :foo, true

include_examples 'should have predicate', :foo?, true

Delegates to the #have_predicate matcher (see Core/#have_predicate, above) and passes if the actual object responds to the specified predicate. If a value is specified, the object must respond to the predicate and return the specified value, which must be true or false. Alternatively, you can set a proc as the expected value, which can contain a comparison, an RSpec expectation, or a more complex expression:

include_examples 'should have predicate', :bar, ->() { a_boolean }

include_examples 'should have predicate', :baz, ->(value) { value == true }

Should Have Property

include_examples 'should have property', :foo, 42

Delegates to the #have_property matcher (see Core/#have_property, above) and passes if the actual object responds to the specified reader and writer methods. If a value is specified, the object must respond to the property and return the specified value. Alternatively, you can set a proc as the expected value, which can contain a comparison, an RSpec expectation, or a more complex expression:

include_examples 'should have property', :bar, ->() { an_instance_of(String) }

include_examples 'should have property', :baz, ->(value) { value.count = 3 }

You can also set the :allow_private option to allow the examples to match a private reader and/or writer method:

include_examples 'should have property', :foo, :allow_private => true

include_examples 'should have property', :foo, 42, :allow_private => true

Should Have Private Property

include_examples 'should have private property', :foo

include_examples 'should have private property', :foo, 42

Passes if the actual object has the specified private or protected property reader and writer, and fails if the actual object does not have the specified reader and writer or if the specified reader or writer is a public method. If a value is specified, the value of the private reader must match the specified value.

Should Have Reader

include_examples 'should have reader', :foo, 42

Delegates to the #have_reader matcher (see Core/#have_reader, above) and passes if the actual object responds to the specified property reader. If a value is specified, the object must respond to the property and return the specified value. Alternatively, you can set a proc as the expected value, which can contain a comparison, an RSpec expectation, or a more complex expression:

include_examples 'should have reader', :bar, ->() { an_instance_of(String) }

include_examples 'should have reader', :baz, ->(value) { value.count = 3 }

You can also set the :allow_private option to allow the examples to match a private reader method:

include_examples 'should have reader', :foo, :allow_private => true

include_examples 'should have reader', :foo, 42, :allow_private => true

Should Not Have Reader

include_examples 'should not have reader', :foo

Delegates to the #have_reader matcher (see Core/#have_reader, above) and passes if the actual object does not respond to to the specified property reader.

Should Have Private Reader

include_examples 'should have private reader', :foo

include_examples 'should have private reader', :foo, 42

Passes if the actual object has the specified private or protected property reader, and fails if the actual object does not have the specified reader or if the specified reader is a public method. If a value is specified, the value of the private reader must match the specified value.

Should Have Writer

include_examples 'should have writer', :foo=

Delegates to the #have_writer matcher (see Core/#have_writer, above) and passes if the actual object responds to the specified property writer.

You can also set the :allow_private option to allow the examples to match a private writer method:

include_examples 'should have writer', :foo=, :allow_private => true

Should Not Have Writer

include_examples 'should not have writer', :foo=

Delegates to the #have_writer matcher (see Core/#have_writer, above) and passes if the actual object does not respond to to the specified property writer.

Should Have Private Writer

include_examples 'should have private writer', :foo=

Passes if the actual object has the specified private or protected property writer, and fails if the actual object does not have the specified writer or if the specified writer is a public method.

RSpec Matcher Examples

These examples are used for validating custom RSpec matchers. They are used internally by RSpec::SleepingKingStudios to verify the functionality of the new and modified matchers.

require 'rspec/sleeping_king_studios/examples/rspec_matcher_examples'

RSpec.describe MyCustomMatcher do
  include RSpec::SleepingKingStudios::Examples::RSpecMatcherExamples

  # You can use the custom shared examples here.
end # describe

The #instance or subject for these examples should be an instance of a class matching the RSpec matcher API. For example, consider a matcher that checks if a number is a multiple of another number. This matcher would be used as follows:

expect(12).to be_a_multiple_of(3)
#=> true

expect(14).to be_a_multiple_of(3)
#=> false

Therefore, the #instance or subject should be defined as BeAMultipleMatcher.new(3). If the custom matcher has additional fluent methods or options, these can be added to the instance as well, e.g. expect(15).to be_a_multiple_of(3).and_of(5) would be tested as BeAMultipleMatcher.new(3).and_of(5).

In addition, all of these examples require a defined #actual method in the example group containing the object to be tested. The actual object is the object used in the expectation. In the above examples, the actual object is 12 in the first example, and 14 in the second. You can define the #actual method using #let(), e.g. let(:actual) { Object.new }.

Putting it all together:

require 'rspec/sleeping_king_studios/examples/rspec_matcher_examples'

RSpec.describe BeAMultipleOfMatcher do
  include RSpec::SleepingKingStudios::Examples::RSpecMatcherExamples

  let(:instance) { BeAMultipleOfMatcher.new(3) }

  describe 'with a valid number' do
    let(:actual) { 15 }

    # Include examples here.

    describe 'with a second factor' do
      let(:instance) { BeAMultipleOfMatcher.new(3).and_of(5) }

      # Include examples here.
    end # describe
  end # describe
end # describe

Passes With A Positive Expectation

include_examples 'passes with a positive expectation'

Verifies that the instance matcher will pass with a positive expectation (e.g. expect().to). Equivalent to verifying the result of the following:

expect(actual).to match_my_custom_matcher(*expected_values)
#=> passes

Passes With A Negative Expectation

include_examples 'passes with a negative expectation'

Verifies that the instance matcher will pass with a negative expectation (e.g. expect().not_to). Equivalent to verifying the result of the following:

expect(actual).not_to match_my_custom_matcher(*expected_values)
#=> passes

Fails With A Positive Expectation

include_examples 'fails with a positive expectation'

Verifies that the instance matcher will fail with a positive expectation (e.g. expect().to), and have the expected failure message. Equivalent to verifying the result of the following:

expect(actual).to match_my_custom_matcher(*expected_values)
#=> fails

In addition, verifies the #failure_message of the matcher by comparing it against a #failure_message method in the example group. This should be defined using let(:failure_message) { 'expected to match' }.

The behavior if the example group does not define #failure_message depends on the value of the RSpec.configure.sleeping_king_studios.examples.handle_missing_failure_message_with option (see Configuration, above). Accepted values are :ignore, :pending (default; marks the example as pending), and :exception (raises an exception).

Fails With A Negative Expectation

include_examples 'fails with a negative expectation'

Verifies that the instance matcher will fail with a negative expectation (e.g. expect().not_to), and have the expected failure message. Equivalent to verifying the result of the following:

expect(actual).not_to match_my_custom_matcher(*expected_values)
#=> fails

In addition, verifies the #failure_message_when_negated of the matcher by comparing it against a #failure_message_when_negated method in the example group. This should be defined using let(:failure_message_when_negated) { 'expected not to match' }.

See Fails With A Positive Expectation, above, for behavior when the example group does not define #failure_message_when_negated.

License

RSpec::SleepingKingStudios is released under the MIT License.

About

A collection of RSpec patches and custom matchers.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •