TDD by example (4) -- 变招

先看to-do list

  随机生成答案
  检查输入是否合法 
   判断猜测结果
  记录历史猜测数据并显示 
  判断猜测次数,如果满6次但是未猜对则判负 

  如果4个数字全中,则判胜  
  实现IRandomIntGenerator

判断猜测次数,如果满6次但是未猜对则判负

我们先理一下思路,如何记录和判断猜测次数和判负。第一种方式是在调用Game类的地方记录和判断,这个类扮演协调者的角色,很可能就是main或类似的东西,他会处理输入输出,调用Game的方法;第二种方式是让Game类自己保存猜测的次数,并在达到6次仍未猜对的情况下出发失败事件;考虑到Game这个类名,判断胜负本来就应该是它的责任,因此这里选择第二种方法。然而这不代表在你的实现里也一定要选这种方式,因为设计本身没有一定的对错,需要综合考虑各种因素,因此我选择第二种方式的原因未必一定也是你选择他的原因。

首先,写测试。测什么呢?给定一个答案,猜测6次全错,应该能够得到游戏失败的通知。至于通知的方式我也有两种选择,一种是用接口实现,一种是用委托,这是典型的Observer模式。这里我用接口实现。

定义一个接口

public   interface  IGameObserver
{
    
void  GameOver();
}

然后用mock来模拟接口的实现以便测试

[Test]
public   void  should_fail_game_when_guess_six_times_and_still_wrong()
{
    var mockObserver 
=   new  Mock < IGameObserver > ();
    var game 
=   new  Game(answerGenerator, mockObserver.Object);
    var wrongGuess 
=   new [] { 5 6 7 8 };
    
for  ( int  i  =   0 ; i  <   5 ; i ++ )
    {
        game.Guess(wrongGuess);
    }
    mockObserver.Verify(m 
=>  m.GameOver(), Times.Never());
    game.Guess(wrongGuess);
    mockObserver.Verify(m 
=>  m.GameOver(), Times.Exactly( 1 ));
}

在这里,我首先mock了一个IGameObserver的对象,然后传给Game类,接下来作5次错误的猜测,验证GameOver并没有被调用,然后猜测第6次,验证GameOver被调用。需要注意的是,我修改了Game构造函数的接口,添加了一个参数,因此前面我们在TestGame类中写的所有测试,都要在创建Game实例时多穿一个null。如:

[Test]
public   void  should_return_0A0B_when_no_number_is_correct()
{
    Assert.That(
new  Game(answerGenerator,  null ).Guess( new [] { 5 6 7 8 }), Is.EqualTo( " 0A0B " ));
}

Game的构造函数现在变成

public  Game(IAnswerGenerator answerGenerator, IGameObserver observer)
{
    
this .answer  =  answerGenerator.Generate();
}

好了,编译通过,运行测试,以前的测试都通过,这个测试失败,说明我们没有破坏以前的东西,接下来在Game类中添加代码

public   class  Game
{
    
private   readonly  IGameObserver observer;
    
private   readonly   int [] answer;
    
private   int  guessTimes  =   0 ;

    
public  Game(IAnswerGenerator answerGenerator, IGameObserver observer)
    {
        
this .observer  =  observer;
        
this .answer  =  answerGenerator.Generate();
    }

    
public   string  Guess( int [] guess)
    {
        guessTimes
++ ;
        
int  aCount  =   0 ;
        
int  bCount  =   0 ;
        
for  ( int  i  =   0 ; i  <  guess.Length; i ++ )
        {
            
if  (answer[i]  ==  guess[i])
            {
                aCount
++ ;
            }
            
else   if  (answer.Contains(guess[i]))
            {
                bCount
++ ;
            }
        }
        
string  result  =   string .Format( " {0}A{1}B " , aCount, bCount);
        
if (guessTimes >= 6   &&  result  !=   " 4A0B " )
        {
            
if (observer != null )
            {
                observer.GameOver();
            }
        }
        
return  result;
    }
}

这里我们添加了一个记录猜测次数的字段,并在每次猜测的时候累加,如果猜测的次数等于或超过6次,而且猜测的结果还不对的话,就会调用GameOver。好的,运行测试,所有测试都通过,看来我们已经完成了这个功能,而且没有破坏以前实现的功能。接下来是重构时间,看看代码那里可以改进。这里“6”是一个magic number,“4A4B”也是一个,对observer的调用可以抽取一个方法出来……代码如下

public   class  Game
{
    
private   readonly  IGameObserver observer;
    
private   readonly   int [] answer;
    
private   int  guessTimes  =   0 ;
    
const   string  CorrectGuess  =   " 4A0B " ;
    
const   int  MaxGuessTimes  =   6 ;

    
public  Game(IAnswerGenerator answerGenerator, IGameObserver observer)
    {
        
this .observer  =  observer;
        
this .answer  =  answerGenerator.Generate();
    }

    
public   string  Guess( int [] guess)
    {
        
string  result  =  GetGuessResult(guess);
        guessTimes
++ ;
        
if (IsGameOver(result))
        {
            OnGameOver();
        }
        
return  result;
    }

    
private   bool  IsGameOver( string  result)
    {
        
return  guessTimes  >=  MaxGuessTimes  &&  result  !=  CorrectGuess;
    }

    
private   string  GetGuessResult( int [] guess)
    {
        
int  aCount  =   0 ;
        
int  bCount  =   0 ;
        
for  ( int  i  =   0 ; i  <  guess.Length; i ++ )
        {
            
if  (answer[i]  ==  guess[i])
            {
                aCount
++ ;
            }
            
else   if  (answer.Contains(guess[i]))
            {
                bCount
++ ;
            }
        }
        
return   string .Format( " {0}A{1}B " , aCount, bCount);
    }

    
protected   void  OnGameOver()
    {
        
if (observer != null )
        {
            observer.GameOver();
        }
    }
}

在这里我提出了一个方法GetGuessResult,代码就是之前Guess方法的代码,这样做的目的是使Guess方法中的每一行代码都在一个抽象程度上,保持函数短小,易于理解。

完整的测试代码

ContractedBlock.gif ExpandedBlockStart.gif Code
[TestFixture]
public class TestGame
{
    
private IAnswerGenerator answerGenerator;

    [SetUp]
    
public void Setup()
    {
        var mockAnswerGenerator 
= new Mock<IAnswerGenerator>();
        mockAnswerGenerator.Setup(generator 
=> generator.Generate()).Returns(new[] {1234});
        answerGenerator 
= mockAnswerGenerator.Object;
    }


    [Test]
    
public void should_fail_game_when_guess_six_times_and_still_wrong()
    {
        var mockObserver 
= new Mock<IGameObserver>();
        var game 
= new Game(answerGenerator, mockObserver.Object);
        var wrongGuess 
= new[] {5678};
        
for (int i = 0; i < 5; i++)
        {
            game.Guess(wrongGuess);
        }
        mockObserver.Verify(m 
=> m.GameOver(), Times.Never());
        game.Guess(wrongGuess);
        mockObserver.Verify(m 
=> m.GameOver(), Times.Exactly(1));
    }

    [Test]
    
public void should_return_0A0B_when_no_number_is_correct()
    {
        Assert.That(
new Game(answerGenerator, null).Guess(new[] {5678}), Is.EqualTo("0A0B"));
    }

    [Test]
    
public void should_return_0A2B_when_two_numbers_are_correct_but_positions_are_not_correct()
    {
        Assert.That(
new Game(answerGenerator, null).Guess(new[] {3456}), Is.EqualTo("0A2B"));
    }

    [Test]
    
public void should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too()
    {
        Assert.That(
new Game(answerGenerator, null).Guess(new[] {1567}), Is.EqualTo("1A0B"));
    }

    [Test]
    
public void should_return_2A2B_when_two_numbers_are_pisition_correct_and_two_are_nunmber_correct()
    {
        Assert.That(
new Game(answerGenerator, null).Guess(new[] {1243}), Is.EqualTo("2A2B"));
    }

    [Test]
    
public void should_return_4A0B_when_all_numbers_are_pisition_correct()
    {
        Assert.That(
new Game(answerGenerator, null).Guess(new[] {1234}), Is.EqualTo("4A0B"));
    }
}

 

使用委托的方式与接口的方式类似,只是在mock的设置上略有不同

首先,测试需要做些修改

private   interface  IGameObserver
{
    
void  GameOver();
}
[Test]
public   void  should_fail_game_when_guess_six_times_and_still_wrong()
{
    var mockObserver 
=   new  Mock < IGameObserver > ();
    var game 
=   new  Game(answerGenerator);
    game.GameOver 
+=  mockObserver.Object.GameOver;
    var wrongGuess 
=   new [] { 5 6 7 8 };
    
for  ( int  i  =   0 ; i  <   5 ; i ++ )
    {
        game.Guess(wrongGuess);
    }
    mockObserver.Verify(m 
=>  m.GameOver(), Times.Never());
    game.Guess(wrongGuess);
    mockObserver.Verify(m 
=>  m.GameOver(), Times.Exactly( 1 ));
}

这里,我们不需要再修改Game类的构造函数了,IGameObserver也不再是实现的一部分,而是一个仅用于测试的接口,测试逻辑还是一样的

Game类也需要修改,需要声明GameOver事件,并在Guess中调用

public   class  Game
{
    
private   readonly   int [] answer;
    
private   int  guessTimes  =   0 ;
    
const   string  CorrectGuess  =   " 4A0B " ;
    
const   int  MaxGuessTimes  =   6 ;

    
public   delegate   void  GameEventHandler();
    
public   event  GameEventHandler GameOver;
    
    
public  Game(IAnswerGenerator answerGenerator)
    {
        
this .answer  =  answerGenerator.Generate();
    }

    
public   string  Guess( int [] guess)
    {
        
string  result  =  GetGuessResult(guess);
        guessTimes
++ ;
        
if (IsGameOver(result))
        {
            OnGameOver();
        }
        
return  result;
    }

    
private   bool  IsGameOver( string  result)
    {
        
return  guessTimes  >=  MaxGuessTimes  &&  result  !=  CorrectGuess;
    }

    
private   string  GetGuessResult( int [] guess)
    {
        
int  aCount  =   0 ;
        
int  bCount  =   0 ;
        
for  ( int  i  =   0 ; i  <  guess.Length; i ++ )
        {
            
if  (answer[i]  ==  guess[i])
            {
                aCount
++ ;
            }
            
else   if  (answer.Contains(guess[i]))
            {
                bCount
++ ;
            }
        }
        
return   string .Format( " {0}A{1}B " , aCount, bCount);
    }

    
protected   void  OnGameOver()
    {
        
if (GameOver != null )
        {
            GameOver();
        }
    }
}

测试代码也可以重构一下了,将Game的创建提到Setup中

[TestFixture]
public   class  TestGame
{
    
private  Game game;

    [SetUp]
    
public   void  Setup()
    {
        var mockAnswerGenerator 
=   new  Mock < IAnswerGenerator > ();
        mockAnswerGenerator.Setup(generator 
=>  generator.Generate()).Returns( new [] { 1 2 3 4 });
        var answerGenerator 
=  mockAnswerGenerator.Object;
        game 
=   new  Game(answerGenerator);
    }

    
public interface  IGameObserver
    {
        
void  GameOver();
    }
    [Test]
    
public   void  should_fail_game_when_guess_six_times_and_still_wrong()
    {
        var mockObserver 
=   new  Mock < IGameObserver > ();
        game.GameOver 
+=  mockObserver.Object.GameOver;
        var wrongGuess 
=   new [] { 5 6 7 8 };
        
for  ( int  i  =   0 ; i  <   5 ; i ++ )
        {
            game.Guess(wrongGuess);
        }
        mockObserver.Verify(m 
=>  m.GameOver(), Times.Never());
        game.Guess(wrongGuess);
        mockObserver.Verify(m 
=>  m.GameOver(), Times.Exactly( 1 ));
    }

    [Test]
    
public   void  should_return_0A0B_when_no_number_is_correct()
    {
        Assert.That(game.Guess(
new [] { 5 6 7 8 }), Is.EqualTo( " 0A0B " ));
    }

    [Test]
    
public   void  should_return_0A2B_when_two_numbers_are_correct_but_positions_are_not_correct()
    {
        Assert.That(game.Guess(
new [] { 3 4 5 6 }), Is.EqualTo( " 0A2B " ));
    }

    [Test]
    
public   void  should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too()
    {
        Assert.That(game.Guess(
new [] { 1 5 6 7 }), Is.EqualTo( " 1A0B " ));
    }

    [Test]
    
public   void  should_return_2A2B_when_two_numbers_are_pisition_correct_and_two_are_nunmber_correct()
    {
        Assert.That(game.Guess(
new [] { 1 2 4 3 }), Is.EqualTo( " 2A2B " ));
    }

    [Test]
    
public   void  should_return_4A0B_when_all_numbers_are_pisition_correct()
    {
        Assert.That(game.Guess(
new [] { 1 2 3 4 }), Is.EqualTo( " 4A0B " ));
    }
}

 

至此,我们又完成了一个任务,更新一下to-do list

  随机生成答案
  检查输入是否合法 
   判断猜测结果
  记录历史猜测数据并显示 
  判断猜测次数,如果满6次但是未猜对则判负  

  如果4个数字全中,则判胜  
  实现IRandomIntGenerator

 

相关文章

如何开始TDD

测试驱动开发之可执行文档

三张卡片帮你记住TDD原则 

 

 

转载于:https://www.cnblogs.com/wangyh/archive/2009/07/13/TDD-by-example-4.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值