AN ILLUSTRATIVE EXAMPLE We're going to consider a simple, somewhat contrived example of a situation where mocks can help. First we will do it the hard way: creating our mocks entirely by hand. Later we will explore excellent tools for automating much of the business of mock creation and setup. Our example involves writing an adventure game in which the player tries to rid the world of foul creatures such as Orcs. When a player attacks an Orc they roll a 20-sided die to see if they hit or not. A roll of 13 or higher is a hit, in which case they roll the 20-sided die again to determine the effect of the hit. If the initial roll was less than 13, they miss. Our task at the moment is to test the code in Player that governs this process. Here's Player (I know, we aren't test driving this code. ..assume we inherited it):
1
public
classPlayer {
2
Die myD20
=
null
;
3
4
public
Player(Die d20) {
5
myD20
=
d20;
6
}
7
8
public
boolean
attack(Orc anOrc) {
9
if
(myD20.roll()
10
>=
13
) {
11
return
hit(anOrc);
12
}
else
{
13
return
miss();
14
}
15
}
16
17
private
boolean
hit(OrcanOrc) {
18
anOrc.injure(myD20.roll());
19
return
true
;
20
}
21
22
private
boolean
miss() {
23
return
false
;
24
}
25
}
26
Here's the initial take at a test. We'll start simply with the case where the attack misses. We'll assume that a Die class already exists:
1
public
class
Die {
2
private
int
sides
=
0
;
3
private
Random generator
=
null
;
4
5
6
public
Die(
int
numberOfSides) {
7
sides
=
numberOfSides;
8
generator
=
newRandom();
9
}
10
11
public
int
roll() {
12
return
generator.nextInt(sides)
+
1
;
13
}
14
}
15
Here's a first stab at the test for a missed attack:
1
public
void
testMiss() {
2
Die d20
=
new
Die(
20
);
3
Player badFighter
=
new
Player(d20);
4
Orc anOrc
=
new
Orc();
5
assertFalse(
"
Attack should have missed.
"
,badFighter.attack(anOrc));
6
}
7
The problem is that there is a random number generator involved. Sometimes the test passes, other times it fails. We need to be able to control the return value in order to control the preconditions for the test. This is a case where we cannot (or rather, should not) get the actual test resource into the state we need for the test. So instead, we use a simple mock object for that. Specifically, we can mock the Die class to return the value we want. But first we need to extract an interface from Die, and use it in place of Die:
1
public
interface
Rollable {
2
int
roll();
3
}
4
5
6
public
class
Die
implements
Rollable {
7
//
. . .
8
}
9
10
public
classPlayer {
11
Rollable myD20
=
null
;
12
public
Player(Rollable d20) {
13
myD20
=
d20;
14
}
15
//
. . .
16
}
17
Now we can create a mock for a 20-sided die that always returns a value that will cause an attack to miss, say, 10:
1
public
class
MockD20FailingAttack
implements
Rollable {
2
public
int
roll() {
3
return
10
;
4
}
5
}
6
Now we use the mock in our test:
1
public
void
testMiss() {
2
Rollable d20
=
new
MockD20FailingAttack();
3
Player badFighter
=
new
Player(d20);
4
Orc anOrc
=
new
Orc();
5
assertFalse(
"
Attack should have missed.
"
,badFighter.attack(anOrc));
6
}
7
There, the test always passes now. Next, we write a corresponding test with a successful attack:
1
public
void
testHit() {
2
Rollable d20
=
new
MockD20SuccessfulAttack();
3
Player goodFighter
=
new
Player(d20);
4
Orc anOrc
=
new
Orc();
5
assertTrue(
"
Attack should have hit.
"
,goodFighter.attack(anOrc));
6
}
7
This requires a new mock:
1
public
class
MockD20SuccessfulAttack
implements
Rollable {
2
public
int
roll() {
3
return
18
;
4
}
5
}
6
Now, these two mocks are almost identical, so we can refactor and merge them into a single parameterized class:
1 public class MockDie implements Rollable { 2 private int returnValue; 3 4 public MockDie(int constantReturnValue) { 5 returnValue = constantReturnValue; 6 } 7 8 public int roll() { 9 return returnValue; 10 } 11 } 12
Our tests are now:
1
public
void
testMiss() {
2
Rollable d20
=
new
MockDie(
10
);
3
Player badFighter
=
new
Player(d20);
4
Orc anOrc
=
new
Orc();
5
assertFalse(
"
Attack should have missed.
"
,badFighter.attack(anOrc));
6
}
7
8
public
void
testHit() {
9
Rollable d20
=
new
MockDie(
18
);
10
Player goodFighter
=
new
Player(d20);
11
Orc anOrc
=
new
Orc();
12
assertTrue(
"
Attack should have hit.
"
,goodFighter.attack(anOrc));
13
}
14
Next, we want to write tests for the cases where an attack hurts the Orc, and where it kills it. For this we will need to extend MockDie so that we can specify a sequence of return values (successful attack followed by the amount of damage). In order to maintain the current behavior, MockDie repeatedly loops through the return value sequence as roll() is called.
1
public
class
MockDie
implements
Rollable {
2
private
Vector returnValues
=
new
Vector();
3
private
int
nextReturnedIndex
=
0
;
4
5
public
MockDie() {
6
}
7
8
public
MockDie(
int
constantReturnValue) {
9
addRoll(constantReturnValue);
10
}
11
12
public
void
addRoll(
int
returnValue) {
13
returnValues.add(
new
Integer(returnValue));
14
}
15
16
public
int
roll()
17
{
18
int
val
=
((Integer)returnValues.get(nextReturnedIndex
++
)).intValue();
19
if
(nextReturnedIndex
>=
returnValues.size()) {
20
nextReturnedIndex
=
0
;
21
}
22
return
val;
23
}
24
}
25
Using the Rollable interface allows us to easily and cleanly create mocks without impacting the Player class at all. We can see this in the class diagram shown in Figure 7.1.
[View full size image]
So far we've written simple mocks, really just stubs that return some predefined values in response to a method call. Now we'll start writing and using a real mock, one that expects and can verify specific calls. Again, so far, we're doing it all by hand. First, consider this implementation of Orc and Game:
1 public class Orc { 2 private Game game = null; 3 private int health = 0; 4 5 public Orc (Game theGame, int hitPoints) 6 { 7 game = theGame; 8 health = hitPoints; 9 } 10 11 public void injure(int damage) { 12 health 13 -=damage; 14 if (health 15 <=0) { 16 die(); 17 } 18 } 19 20 private void die() { 21 game.hasDied(this); 22 } 23 24 public boolean isDead() { 25 return health <=0; 26 } 27 } 28 29 public interfaceGame { 30 void hasDied(Orc orc); 31 } 32
Now we can write tests like this:
1
public
void
testNoKill()
2
{
3
MockGame mockGame
=
new
MockGame();
4
Orc strongOrc
=
new
Orc(mockGame,
30
);
5
6
MockDie d20
=
new
MockDie();
7
d20.addRoll(
18
);
8
d20.addRoll(
10
);
9
10
Player fighter
=
new
Player(d20);
11
fighter.attack(strongOrc);
12
assertFalse(
"
The orc should not have died.
"
,strongOrc.isDead());
13
mockGame.verify();
14
}
15
16
public
void
testKill() {
17
MockGame mockGame
=
new
MockGame();
18
Orc weakOrc
=
new
Orc(mockGame,
10
);
19
mockGame.expectHasDied(weakOrc);
20
21
MockDie d20
=
new
MockDie();
22
d20.addRoll(
18
);
23
d20.addRoll(
15
);
24
25
Player fighter
=
new
Player(d20);
26
fighter.attack(weakOrc);
27
assertTrue(
"
The orc should be dead.
"
,weakOrc.isDead());
28
mockGame.verify();
29
}
30
The one thing that is missing is MockGame. Here it is:
1
public
class
MockGame
implements
Game
2
{
3
private
Orc deadOrc
=
null
;
4
private
Orc orcExpectedToDie
=
null
;
5
6
public
void
hasDied(Orc orc) {
7
if
(orc
!=
orcExpectedToDie) {
8
Assert.fail(
"
Unexpected orc died.
"
);
9
}
10
11
if
(deadOrc
!=
null
) {
12
Assert.fail(
"
Only expected one dead orc.
"
);
13
}
14
15
deadOrc
=
orc;
16
}
17
18
public
void
expectHasDied(Orc doomedOrc) {
19
orcExpectedToDie
=
doomedOrc;
20
}
21
22
23
24
25
public
void
verify () {
26
Assert.assertEquals(
"
Doomed Orc didn't die.
"
,orcExpectedToDie, deadOrc);
27
}
28
}
29
When an Orc dies it reports the fact to the game. In the case of the tests, this is a MockGame that checks that the dead orc is the one that was expected and that this is the only dead orc so far. If either of these checks fail, then the mock causes the test to fail immediately by calling Asert.fail().When the mock is set up by the test it is told what orc it should expect to die. At the end of the test the MockGame can be asked to verify that the expected Orc died. This is a trivial mock, but it should give you a taste of what is possible. Figure 7.2 shows this part of the class diagram, while Figure 7.3 shows the sequence diagram for testKill().
[View full size image]
You can now easily see that as our mocks get more complex, they get harder to write and maintain, if we write them from scratch each time. The folks that evolved the mock objects concept found this out quickly, and as happens when you find yourself writing similar code a lot, they developed a framework for creating mocks and tools to make the job easier. Again, we'll have more on that later. |