Skip to content

Tutorial: Immediate Given

jimweirich edited this page Apr 17, 2013 · 9 revisions

This is part of the RSpec/Given Tutorial.

Immediate Given

Let's continue with a more complex story. This one involves two characters, an attacker and a defender, and specifies how damage is calculated during an attack.

Feature: Character Can Be Damaged

As an attacker I want to be able to damage my enemies so that they will die and I will live

  • If attack is successful, other character takes 1 point of damage when hit
  • If a roll is a natural 20 then a critical hit is dealt and the damage is doubled
  • when hit points are 0 or less, the character is dead

The Givens

Let's start with two characters as our givens.

describe "Character can be damaged" do
  Given(:attacker) { Character.new("Attacker") }
  Given(:defender) { Character.new("Defender") }
  When { attacker.attack(defender, 19) }
  Then { defender.hit_points.should == 4 }
end

We decided to use the method attack to invoke the attack. We pass the defender to the attack as an argument along with the dice roll value.

We picked "19" as a dice value because it is a high dice roll that is sure to hit (given more or less equal opponents). The "4" value is one less than the original hit points, the expected value of the defender after the successful attack is made.

This is a perfectly workable spec, but the presence of the "magic" numbers is a bit bothersome. Let's fix that.

Naming Constants

Same spec, but with the constants named:

describe "Character can be damaged" do
  WINNING_DICE_ROLL = 19

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

  When { attacker.attack(defender, WINNING_DICE_ROLL) }

  Then { defender.hit_points.should == original_hp - 1 }
end

The winning dice roll is specified as a constant. We could have said Given(:winning_dice_roll) { 19 }, but using a constant emphasizes that this is a value that doesn't change.

By using a Given for the original hit points, we can capture the actual value of the original from the character object itself and do not have to depend on knowing that value a priori. The Then clause uses original_hp-1 to clearly show that the hit points decrease by one.

I think this spec shows our intent much better than the original.

Unfortunately, it does not work.

Fixing the original_hp

Given clauses with variable names define a lazy evaluation. The value of the variable is not calculated until the moment it is needed. The first time it is referenced, the value is computed and then stored for later use (it is only calculated once per Then clause).

The problem with our code is that original_hp is not referenced until the Then clause which runs after the When clause. By that time it is too late to capture the original hit points of the defender.

We can fix that problem by using a Given! clause. Given! is just like Given, except it is not lazy, it is always computed at the same time all the non-variable Given's are computed. This allows us to capture the original_hp at the proper time.

Our improved spec now looks like this.

describe "Character can be damaged" do
  WINNING_DICE_ROLL = 19

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

  When { attacker.attack(defender, WINNING_DICE_ROLL) }

  Then { defender.hit_points.should == original_hp - 1 }
end

-- Previous: Tutorial: Simple Spec
Next: Tutorial: Inherited When