Friday, October 17, 2014

Superlet

Lets say you are testing your app superhero in RSpec:

RSpec.describe Superhero do
  subject(:superhero) { Superhero.new }

  it { is_expected.to be_super }
end


This superhero is sad because he has no powers though. So lets try giving him x-ray vision:

RSpec.describe Superhero do
  subject(:superhero) { Superhero.new }

  it { is_expected.to be_super }
  it { is_expected.not_to have_xray_vision }

  context "when given x-ray vision" do
    subject(:superhero) { Superhero.new(:xray_vision) }
    it { is_expected.to have_xray_vision }
    it { is_expected.to be_super }
  end
end


And now we want x-ray vision and flight:

RSpec.describe Superhero do
  subject(:superhero) { Superhero.new }

  it { is_expected.to be_super }
  it { is_expected.not_to have_xray_vision }
  it { is_expected.not_to have_flight }

  context "when given x-ray vision" do
    subject(:superhero) { Superhero.new(:xray_vision) }

    it { is_expected.to be_super }
    it { is_expected.to have_xray_vision }
    it { is_expected.not_to have_flight }

    context "when given flight" do
      subject(:superhero) { Superhero.new(:xray_vision, :flight) }

      it { is_expected.to be_super }
      it { is_expected.to have_xray_vision }
      it { is_expected.to have_flight }
    end
  end
end


This test looks pretty nice, but we would like it to be dry in case we want to change the names of the powers to fight a new supervillain. First lets use the power of arrays and the splat:

RSpec.describe Superhero do
  subject(:superhero) { Superhero.new *powers }
  let(:powers) { Array.new }

  it { is_expected.to be_super }
  it { is_expected.not_to have_xray_vision }
  it { is_expected.not_to have_flight }

  context "when given x-ray vision" do
    let(:powers) { [:xray_vision] }

    it { is_expected.to be_super }
    it { is_expected.to have_xray_vision }
    it { is_expected.not_to have_flight }

    context "when given flight" do
      let(:powers) { [:xray_vision, :flight] }

      it { is_expected.to be_super }
      it { is_expected.to have_xray_vision }
      it { is_expected.to have_flight }
    end
  end
end


Next we can use the super power of lets, super():

RSpec.describe Superhero do
  subject(:superhero) { Superhero.new *powers }
  let(:powers) { Array.new }

  it { is_expected.to be_super }
  it { is_expected.not_to have_xray_vision }
  it { is_expected.not_to have_flight }

  context "when given x-ray vision" do
    let(:powers) { super() << :xray_vision }

    it { is_expected.to be_super }
    it { is_expected.to have_xray_vision }
    it { is_expected.not_to have_flight }

    context "when given flight" do
      let(:powers) { super() << :flight }

      it { is_expected.to be_super }
      it { is_expected.to have_xray_vision }
      it { is_expected.to have_flight }
    end
  end
end


Woah, now we have perfectly dried up our tests, but how on Krypton does this work?

#describe and #context are aliases and both create classes descended from RSpec::Core::ExampleGroup, but the magic is that each example group is also a subclass of the example group that it is contained in so any methods defined within an example group can be accessed by its children. The innermost example group defined above will return this if you ask for its ancestors:

[RSpec::ExampleGroups::Superhero::WhenGivenXrayVision::WhenGivenFlight, RSpec::ExampleGroups::Superhero::WhenGivenXrayVision, RSpec::ExampleGroups::Superhero, ...]

The other half of this equation is that #let defines a method on the example group it is contained within. This means that every one of the classes listed in that ancestor list has the method #powers defined on it, and through the power of inheritance you can access #powers on the parent example group with the call to super().