TDD- Secton 7 Why Mock Objects?


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]

[1] There are other ways of approaching this, such as creating a subclass that returns the constant, but I like working with interfaces.

 
             
 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.

Figure 7.1. Class diagram of the Player and Die aspects of the example.
[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().

Figure 7.2. Class diagram of the Orc and Game aspects of the example.




Figure 7.3. Sequence diagram of the testKill() test.
[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.

转载于:https://www.cnblogs.com/kapok/archive/2005/11/08/271563.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值