Skip to content

Tutorial: inherited when

jimweirich edited this page Sep 4, 2012 · 10 revisions

This is part of the RSpec/Given Tutorial

Inherited When

Our previous spec (on page Tutorial: Immediate Given) only handles a single condition of the requirements, when the attack succeeds. There is also the case of the critical hit (explicitly called out in the story) and the case of the miss (implied by the story).

Let's add them to our spec using nested context blocks.

describe "Character can be damaged" do
  LOSING_DICE_ROLL = 2
  WINNING_DICE_ROLL = 19
  CRITICAL_DICE_ROLL = 20

  Given(:attacker) { Character.new("Attacker") }
  Given(:defender) { Character.new("Defender") }
  Given!(:original_hp) { defender.hit_points }

  context "when the attacker misses" do
    When { attacker.attack(defender, LOSING_DICE_ROLL) }
    Then { defender.hit_points.should == original_hp }
  end

  context "when the attacker hits" do
    When { attacker.attack(defender, WINNING_DICE_ROLL) }
    Then { defender.hit_points.should == original_hp - 1 }
  end

  context "when the attacker gets a critical hit" do
    When { attacker.attack(defender, CRITICAL_DICE_ROLL) }
    Then { defender.hit_points.should == original_hp - 2 }
  end
end

Notice how we take advantage of Givens defined in the outer scope in each of the inner scopes. This is a good technique.

Also notice how each of the nested contexts are just a minor variation on the basic test. We change the dice roll and we have a different effect on the change in hit points. However, we have to carefully examine each of the contexts to make sure that is the only change between the contexts. It would be nice if we could make variations stand out a bit more.

Refactoring the Common Actions from the Contexts

describe "Character can be damaged" do
  LOSING_DICE_ROLL = 2
  WINNING_DICE_ROLL = 19
  CRITICAL_DICE_ROLL = 20

  Given(:attacker) { Character.new("Attacker") }
  Given(:defender) { Character.new("Defender") }
  Given!(:original_hp) { defender.hit_points }

  When { attacker.attack(defender, dice_value) }
  Given(:change_in_hp) { defenders.hit_points - original_hp }

  context "when the attacker misses" do
    Given(:dice_value) { LOSING_DICE_ROLL }
    Then { change_in_hp.should == 0 }
  end

  context "when the attacker hits" do
    Given(:dice_value) { WINNING_DICE_ROLL }
    Then { change_in_hp.should == -1 }
  end

  context "when the attacker gets a critical hit" do
    Given(:dice_value) { CRITICAL_DICE_ROLL }
    Then { change_in_hp.should == -2 }
  end
end

The first thing we notice is that the When clause is pulled into the outer block. Each nested context block inherits the When from the outer block and will run it before each of the enclosed Then clauses. By pulling the When clause up, we emphasize that each context block is testing the same thing, namely the attack call.

Secondly we see that the When clause references dice_value, but dice_value is not defined in the outer scope. Instead each inner clause defines its own value for dice_value that will be used by the When clause.

And finally we see that the change_in_hp is defined as a given, defined before the When clause (in the outer level), but since it is lazy-evaled the value isn't calculated until after the When clause is executed.

By using these techniques, the individual nested contexts are extremely minimal and strongly convey just what changes (in input and output) between each of the contexts, making very readable specifications.

The Isolation and Order page summarizes the execution order for Given/When/Then clauses.

-- Previous: Tutorial: Immediate Given
Next: Tutorial: Invariants