-
Notifications
You must be signed in to change notification settings - Fork 61
Tutorial: Immediate Given
This is part of the RSpec/Given Tutorial.
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.
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
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.
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.
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