quarta-feira, 1 de outubro de 2008

Desenvolvimento Orientado por Testes

Nesta primeira parte de uma série de artigos, focalizaremos o uso do Dunit para escrever nossos testes e isto irá ilustrar os benefícios de escrever o teste unitário antes de implementar o código.
O Problema

Considere o seguinte problema. Não passe adiante apressadamente. Tome uns 10 minutinhos pra considerar o seguinte:
Desenhe um diagrama UML ou simplemente aliste as classes que você precisaria pra fazer a seguinte aplicação.
Nós queremos fazer um Jogo da Velha neste exercício. Existem dois jogadores que devem utilizar a aplicação. Um coloca a marca “X” e o outro o “O” na casa desejada. Existem três linhas e três colunas. Primeiro, o usuário deve indicar se o primeiro jogador irá colocar o “X” ou o “O”. Depois, o jogador inicial irá começar marcando a primeira casa. O jogador só poderá utilizar uma casa vazia. Os jogadores se alternam e o jogo continua até que um jogador tenha colocado três marcas iguais em uma linha, diagonal ou coluna ou até que não haja mais nenhuma casa vazia (condição de empate). Se um dos jogadores ganha, o JogoDaVelha deve anunciar isto indicando o nome do ganhador. A aplicação deverá controlar os scores (número de vezes que cada jogador ganhou). A qualquer momento o jogador pode querer ver as estatísticas do jogo.
Gastou seus 10 minutos pensando em como fazer este pequeno jogo? Então, agora pode continuar...
Pensando em classes

Quais as classes necessárias pra desenvolver esta pequena aplicação? Pense um pouco... Alguns diriam Tabuleiro, Casa, Marca, Jogador, Score, etc. E você, quais as classes que você acha necessário neste jogo?
Como escrever os testes

Primeiro pense no que seria necessário testar. Existem pelo menos três coisas para as quais você precisa escrever testes: positivo, negativo e exceção. Quando estiver pensando em uma tarefa, pense do lado positivo, ou seja, o que a aplicação deveria fazer corretamente se tudo estiver na situação ideal. Então, pense pelo lado negativo, ou seja, o que poderia dar errado e como a aplicação deveria se comportar. A exceção é uma seqüência alternativa de acontecimentos. Pense em como o código deve se comportar nestes casos. O sucesso de um teste positivo é quando o código se comporta como deveria. O sucesso de um teste negativo ou exceção é quando o código se comporta de forma esperada à uma situação negativa ou de exceção.
Onde escrever os testes

Uma vez que não estejamos interessados em testar apenas os métodos públicos de uma classe, mas também os métodos internos (private), devemos escrevê-los dentro da mesma unit, para que tenhamos acesso aos metodos e dados internos da classe. Se este não for o caso, podemos escrever em outra unit ou até mesmo em outro projeto.
Lista de tarefas

O primeiro passo é escrever uma lista de tarefas. Esta lista conterá um ou mais testes. Então, à medida que formos escrevendo os testes, implementando o código e sendo bem sucedidos, vamos marcando cada item da lista e passando pro próximo item. Enquanto escrevemos os testes e o código, pensaremos em outros testes que precisamos fazer. Mas em vez de escrever os testes enquanto eles vêm à mente, coloque-os no fim da lista de tarefas. É importante continuar a trabalhar nos itens da lista que já tem à mao. Então, você pode voltar e dar-lhes a devida atenção.
Podemos começar a codificar, por favor?

Claro, podemos iniciar. Mas, antes, tome o cuidade de providenciar a lista de tarefas. O que devemos testar primeiro? Que tal criar o tabuleiro? Ok. Mas o que fazer depois de criar o tabuleiro? Bem, sempre devemos escrever os nossos testes com as assertivas em mente. Temos que fazer os testes com assertivas para ver se a criação da nossa classe foi bem sucedida. Vamos então à lista de tarefas:
1. Criar Tabuleiro;
Em primeiro lugar, criamos um projeto em branco chamado JogoDaVelha.dpr, no qual daremos suporte ao teste unitário como se segue (observe que não criamos nenhum form ainda, pois iniciaremos o desenvolvimento pelos testes):
program JogoDaVelha;

uses
  Forms,
  TestFramework,
  GUITestRunner;

{$R *.res}

begin
  {$IFDEF TESTING}
  GUITestRunner.RunRegisteredTests;
  {$ELSE}
  Application.Initialize;
  Application.Run;
  {$ENDIF}
end.

Listagem 1.

Você precisará ter instalado o dunit (9.3 ou mais recente) para executar os testes. Nas opções do projeto ativaremos os testes unitários definindo a diretiva TESTING em conditional defines. Quando esta diretiva estiver definida, o sistema irá rodar no modo de testes, caso contrário irá rodar o nosso aplicativo normalmente. Veja na figura abaixo como fica:


clip_image002[6]


Figura 1.


Os testes são classes especializadas herdadas de TTestCase. Estes testes devem ser registrados para que o Framework de testes tenha conhecimento que ele existe. Utilizamos a sessão initialization para registrar os testes. Devemos nos lembrar de referenciar a unit TestFramework na cláusula uses. Como você deve ter notado na listagem 1, quando definimos a diretiva de compilação TESTING, o sistema irá executar os testes que estiverem registrados:


  GUITestRunner.RunRegisteredTests;


Agora vamos escrever o nosso primeiro teste, que residirá na mesma unit da nossa classe para que possamos testar os membros privados da classe. Pra isso criamos uma unit que chamaremos de uTabuleiro.pas e iniciaremos a codificação pelo teste, como se segue:


unit uTabuleiro;

interface

uses
  SysUtils, Classes;

// A definição de nossa classe tabuleiro vem aki
// TODO: Criar classe TTabuleiro

implementation

uses TestFramework;

// *************************************************
// Esta é a classe que efetuará os testes unitários
// *************************************************
type
  TTabuleiroJVTests = class(TTestCase)
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  end;

{ TTabuleiroTests }

// *****************************************************************************
// Este método é chamado no início de cada teste
// pode ser usado pra configurar o teste, criar variáveis de instância, etc.
// *****************************************************************************
procedure TTabuleiroJVTests.SetUp;
begin
  inherited;

end;

// *****************************************************************************
// Este método é chamado no fim de cada teste
// pode ser usado pra liberar os recursos, entre outras coisas
// *****************************************************************************
procedure TTabuleiroJVTests.TearDown;
begin
  inherited;

end;

initialization
  TestFramework.RegisterTest('TTabuleiroJV Tests', TTabuleiroJVTests.Suite);
end.


Listagem 2.


Execute (F9) o sistema para ver se está tudo ok. Se estiver devemos ver uma tela como esta:


clip_image004[6]


Bem, agora que o nosso teste está configurado, registrado e funcional podemos iniciar a codificação do primeiro item da lista (“criar tabuleiro”):


type
  TTabuleiroJVTests = class(TTestCase)
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published // Os testes devem ficar na sessão published
    procedure TestCriarTabuleiro;
  end;

procedure TTabuleiroTests.TestCriarTabuleiro;
var
  lTabuleiro: TTabuleiroJV;
begin
  lTabuleiro := TTabuleiroJV.Create;
  CheckNotNull(lTabuleiro);
  CheckFalse(lTabuleiro.GameOver);
  lTabuleiro.Free;
end;


Se tentarmos compilar a nossa aplicação agora receberemos uma mensagem de erro, afinal ainda nem existe uma classe chamada TTabuleiroJV. Criamos a classe TTabuleiroJV com uma propriedade GameOver:


type
  TTabuleiroJV = class
  private
    function GetGameOver: Boolean;
  public
    property GameOver: Boolean read GetGameOver;
  end;

implementation


{ TTabuleiroJV }

function TTabuleiroJV.GetGameOver: Boolean;
begin
  Result := False; // Neste ponto implementamos apenas o que for estritamente necessário
                   // para o teste passar
end;


Quando executamos novamente e mandamos executar os testes selecionados, obtemos o seguinte resultado:


clip_image006[6]


Figura 2.


Legal, o nosso teste passou! Agora, diante do resultado positivo, ficamos encorajados a continuar. O que vem em seguida? Que outros testes devemos fazer? Vamos incrementar a nossa lista de tarefas:


1. Criar Tabuleiro;
2. Configurar o primeiro jogador;
3. Configurar o primeiro jogador novamente;
4. Configurar o primeiro jogador depois que o jogo é iniciado;

Nós não queremos apenas testar a configuração do primeiro jogador, mas também pensar sobre o que aconteceria caso tentássemos configurá-lo novamente ou durante o jogo em andamento.


Agora pense um pouco: Como devemos implementar o nosso teste #2?


· Que tal tabuleiro.SetPrimeiroJogador(‘Marcos’)?


· Será relevante pro jogo saber que o jogador A é Marcos?


· Pode ser que sim, pode ser que não. Não temos certeza neste momento;


· Por isso vamos nos lembrar do princípio que diz: “Não faça algo que não tem certeza que irá precisar naquele momento”


Então podemos tentar:


tabuleiro.SetPrimeiroJogadorJogaX(true);


Com isto não precisamos nos preocupar sobre os testes nem sobre o jogador usado. E, como temos apenas duas opções (“X” e “O”) podemos usar um boolean pra indicar se o Jogador A (Que ainda nem sabemos quem é...) irá jogar “X” ou não. Mais fácil, não? Bem, uma vez decidido como será o teste, vamos implementá-lo:


type
  TTabuleiroJVTests = class(TTestCase)
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestCriarTabuleiro;
    procedure TestSetPrimeiroJogador;
  end;

procedure TTabuleiroJVTests.TestSetPrimeiroJogador;
var
  lTabuleiro: TTabuleiroJV;
begin
  lTabuleiro := TTabuleiroJV.Create;
  lTabuleiro.PrimeiroJogadorJogaX := True;
  CheckTrue(lTabuleiro.PrimeiroJogadorJogaX);
  lTabuleiro.Free;
end;


Quando tentamos compilar recebemos uma mensagem de erro indicando que a propriedade PrimeiroJogadorJogaX não existe, por isso iremos implementá-la:


  TTabuleiroJV = class
  public
    ...
    property PrimeiroJogadorJogaX: Boolean read GetPrimeiroJogadorJogaX
       write SetPrimeiroJogadorJogaX;
  end;

function TTabuleiroJV.GetPrimeiroJogadorJogaX: Boolean;
begin
  Result := True;
end;

procedure TTabuleiroJV.SetPrimeiroJogadorJogaX(const Value: Boolean);
begin

end;


Lembre-se: Não iremos implementar nada que não seja estritamente necesário pra passar no teste.


Agora executamos o teste novamente e: Tudo verdin... que lindo! E visto que foi bem sucedido, ticamos o teste #2 na nossa lista de tarefas:


1. Criar Tabuleiro;
2. Configurar o primeiro jogador;
3. Configurar o primeiro jogador novamente;
4. Configurar o primeiro jogador depois que o jogo é iniciado;

Agora vamos pro terceiro teste. O que acontece caso tente indicar o primeiro jogador novamente, talvez com um valor diferente? Vamos ao teste:


type
  TTabuleiroJVTests = class(TTestCase)
    ...
  published
    ...
    procedure TestSetPrimeiroJogadorNovamente;
  end;

procedure TTabuleiroJVTests.TestSetPrimeiroJogadorNovamente;
var
  lTabuleiro: TTabuleiroJV;
begin
  lTabuleiro := TTabuleiroJV.Create;
  lTabuleiro.PrimeiroJogadorJogaX := True;
  CheckTrue(lTabuleiro.PrimeiroJogadorJogaX);
  lTabuleiro.PrimeiroJogadorJogaX := False;
  CheckFalse(lTabuleiro.PrimeiroJogadorJogaX);
  lTabuleiro.Free;
end;


Compilamos, tudo ok... Agora vamos executar o teste:


clip_image008[6]


O que aconteceu???! Por que falhou? Falhou porque esperávamos False mas lTabuleiro.PrimeiroJogadorJogaX retornou True. Olhando o código podemos ver o motivo da falha. Ainda não implementamos a propriedade corretamente. Ela vai retornar sempre True. Então mãos à obra. Vamos implementá-la corretamente agora, introduzindo uma variável de instância pra armazenar o valor:


type
  TTabuleiroJV = class
  private
    FProximoJogadorJogaX: Boolean;
    ...
  end;

function TTabuleiroJV.GetPrimeiroJogadorJogaX: Boolean;
begin
  Result := FProximoJogadorJogaX;
end;

procedure TTabuleiroJV.SetPrimeiroJogadorJogaX(const Value: Boolean);
begin
  FProximoJogadorJogaX := Value;
end;


Executamos os testes novamente, e tudo correu bem!


Agora, é uma boa hora pra olhar o código e refatorar. Olhando o código, percebemos que no início de cada teste estamos criando a classe TTabuleiroJV. Isto é uma violação do princípio DRY (Don´t Repeat Yourself). Bem, pra resolver este problema podemos mover a criação do objeto para o método SetUp e atribuindo-o a uma variável de instância, evitando, assim as repetidas instanciações.


Veja como ficou:


// *************************************************
// Esta é a classe que efetuará os testes unitários
// *************************************************
type
  TTabuleiroJVTests = class(TTestCase)
  private
    FTabuleiro: TTabuleiroJV;
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestCriarTabuleiro;
    procedure TestSetPrimeiroJogador;
    procedure TestSetPrimeiroJogadorNovamente;
  end;

{ TTabuleiroJVTests }

// *****************************************************************************
// Este método é chamado no início de cada teste
// pode ser usado pra configurar o teste, criar variáveis de instância, etc.
// *****************************************************************************
procedure TTabuleiroJVTests.SetUp;
begin
  inherited;
  FTabuleiro := TTabuleiroJV.Create;
end;

// *****************************************************************************
// Este método é chamado no fim de cada teste
// pode ser usado pra liberar os recursos, entre outras coisas
// *****************************************************************************
procedure TTabuleiroJVTests.TearDown;
begin
  inherited;
  FreeAndNil(FTabuleiro);
end;

procedure TTabuleiroJVTests.TestCriarTabuleiro;
begin
  CheckNotNull(FTabuleiro);
  CheckFalse(FTabuleiro.GameOver);
end;

procedure TTabuleiroJVTests.TestSetPrimeiroJogador;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  CheckTrue(FTabuleiro.PrimeiroJogadorJogaX);
end;

procedure TTabuleiroJVTests.TestSetPrimeiroJogadorNovamente;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  CheckTrue(FTabuleiro.PrimeiroJogadorJogaX);
  FTabuleiro.PrimeiroJogadorJogaX := False;
  CheckFalse(FTabuleiro.PrimeiroJogadorJogaX);
end;


Executamos o nosso teste novamente, para nos certificarmos de não ter quebrado nada durante o refatoramento. Uma vez que tudo ocorreu bem, continuamos.


É hora de atualizar a nossa lista:


1. Criar Tabuleiro;
2. Configurar o primeiro jogador;
3. Configurar o primeiro jogador novamente;
4. Configurar o primeiro jogador depois que o jogo é iniciado;

O único teste que ficou na lista é aquele que eu ainda não tenho a menor idéia de como irei testá-lo no momento, visto que o jogo ainda não pode ser iniciado. Por isso, vamos deixá-lo um pouco de lado e retornaremos a ele futuramente, no momento apropriado.


Vamos alistar outros testes:


1. Criar Tabuleiro;
2. Configurar o primeiro jogador;
3. Configurar o primeiro jogador novamente;
4. Configurar o primeiro jogador depois que o jogo é iniciado;
5. Marcar a primeira casa;
6. Marcar duas casas em seguida;
7. Marcar três casas em seguida;
8. Colocar uma marca numa casa ocupada;
9. Colocar uma marca numa coluna fora da faixa;
10. Colocar uma marca numa linha fora da faixa;

Vamos escrever o teste #5:


procedure TTabuleiroJVTests.TestMarcarAPrimeiraCasa;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(1, 1);
  CheckTrue(FTabuleiro.CasaContemUmX(1, 1));
end;


A primeira implementação dos métodos segue abaixo:


function TTabuleiroJV.CasaContemUmX(lin, col: Integer): Boolean;
begin
  Result := False;
end;

procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin

end;


Isto resultará na falha do teste #5, visto que ainda não o implementamos completamente. Vamos atingir o verde com o seguinte código:


  TTabuleiroJV = class
  private
    FCasas: array [1..3, 1..3] of string;
  public
    constructor Create;
    procedure MarcarCasa(lin, col: Integer);
    function CasaContemUmX(lin, col: Integer): Boolean;
  end;

constructor TTabuleiroJV.Create;
begin
  FCasas[1,1] := '';
  FCasas[1,2] := '';
  FCasas[1,3] := '';
  FCasas[2,1] := '';
  FCasas[2,2] := '';
  FCasas[2,3] := '';
  FCasas[3,1] := '';
  FCasas[3,2] := '';
  FCasas[3,3] := '';
end;

procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin
  FCasas[lin, col] := 'X';
end;

function TTabuleiroJV.CasaContemUmX(lin, col: Integer): Boolean;
begin
  Result := (FCasas[lin, col] = 'X');
end;


Ao executarmos os testes novamente obtemos sucesso em todos os testes. Vamos dar continuidade ao processo.


Em primeiro lugar, vamos atualizar a nossa lista:


1. Criar Tabuleiro;
2. Configurar o primeiro jogador;
3. Configurar o primeiro jogador novamente;
4. Configurar o primeiro jogador depois que o jogo é iniciado;
5. Marcar a primeira casa;
6. Marcar duas casas em seguida;
7. Marcar três casas em seguida;
8. Colocar uma marca numa casa ocupada;
9. Colocar uma marca numa coluna fora da faixa;
10. Colocar uma marca numa linha fora da faixa;

E agora, vamos implementar o teste #6.


procedure TTabuleiroJVTests.TestMarcarDuasCasasEmSeguida;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(1, 1);
  CheckTrue(FTabuleiro.CasaContemUmX(1, 1));
  FTabuleiro.MarcarCasa(1, 2);
  CheckFalse(FTabuleiro.CasaContemUmX(1, 2));
end;


Este teste irá falhar. Visto que CasaContemUmX(1, 2) irá retornar True, ao passo que esperamos que seja False. Olhando o código percebemos o motivo disto: Não estamos alternando de “X” pra “O” a cada jogada. Portanto, aqui vai a parte do código que corrige esse comportamento errado:


procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin
  FCasas[lin, col] := 'O';
  if FProximoJogadorJogaX then
  begin
    FCasas[lin, col] := 'X';
    FProximoJogadorJogaX := not FProximoJogadorJogaX;
  end;
end;


Ao executarmos os testes novamento obteremos sucesso. Portanto, atualizamos a nossa lista:


6. Marcar duas casas em seguida;


Vamos pro teste #7:


procedure TTabuleiroJVTests.TestMarcarTresCasasEmSeguida;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(2, 1);
  CheckTrue(FTabuleiro.CasaContemUmX(2, 1), '1ª casa');
  FTabuleiro.MarcarCasa(2, 2);
  CheckFalse(FTabuleiro.CasaContemUmX(2, 2), '2ª casa');
  FTabuleiro.MarcarCasa(1, 2);
  CheckTrue(FTabuleiro.CasaContemUmX(1, 2), '3ª casa'); // <- aki="" end="" falha="" pre="">

Poxa! O que foi que deu errado desta vez??? Não estava funcionando tudo direitinho? Por que agora está dando erro?



Analisando o método marcar descobrimos o erro: 



procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin
  FCasas[lin, col] := 'O';
  if FProximoJogadorJogaX then
  begin
    FCasas[lin, col] := 'X';
    FProximoJogadorJogaX := not FProximoJogadorJogaX;
  end;
end;
Veja que a alternação de jogadores só funcionada se o proximo jogador for X. Portanto temos que tirar FProximoJogadorJogaX := not FproximoJogadorJogaX de dentro do if:
procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin
  FCasas[lin, col] := 'O';
  if FProximoJogadorJogaX then
    FCasas[lin, col] := 'X';

  FProximoJogadorJogaX := not FProximoJogadorJogaX;
end;
Viram como é importante ter uma boa bateria de testes? No primeiro e no segundo teste, parecia estar correto. Mas havia um bug escondidinho lá que só percebemos no terceiro teste. Vamos atualizar a nossa lista: 1. Criar Tabuleiro; 2. Configurar o primeiro jogador; 3. Configurar o primeiro jogador novamente; 4. Configurar o primeiro jogador depois que o jogo é iniciado; 5. Marcar a primeira casa; 6. Marcar duas casas em seguida; 7. Marcar três casas em seguida; 8. Colocar uma marca numa casa ocupada; 9. Colocar uma marca numa coluna fora da faixa; 10. Colocar uma marca numa linha fora da faixa; Agora é a vez do teste #8. O sucesso deste teste está na falha. Como assim? Bem, ele não deve permitir que eu marque uma casa já ocupada não é verdade? Neste caso ele deve reportar um erro. Portanto, vamos codificar o teste:
interface

type
  EJogoDaVelhaError = class(Exception);

implementation
.
.
.

procedure TTabuleiroJVTests.TestMarcarUmaCasaOcupada;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(2, 1);
  // Aki dizemos pro framework que estamos aguardando uma exceção do tipo
  // EJogoDaVelhaError.
  // Se esta exceção não ocorrer, o teste irá falhar
  ExpectedException := EJogoDaVelhaError;
  FTabuleiro.MarcarCasa(2, 1);
  ExpectedException := nil;
end;
Ao executarmos o teste veremos que ele realmente falha. Isto acontece porque ainda não implementamos as alterações para verificação desta condição de erro no método MarcarCasa(). Veja como fica:
procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin
  if FCasas[lin, col] <> '' then
    raise EJogoDaVelhaError.Create('A casa já está ocupada');

  FCasas[lin, col] := 'O';
  if FProximoJogadorJogaX then
    FCasas[lin, col] := 'X';

  FProximoJogadorJogaX := not FProximoJogadorJogaX;
end;
Vamos atualizar a nossa lista: 1. Criar Tabuleiro; 2. Configurar o primeiro jogador; 3. Configurar o primeiro jogador novamente; 4. Configurar o primeiro jogador depois que o jogo é iniciado; 5. Marcar a primeira casa; 6. Marcar duas casas em seguida; 7. Marcar três casas em seguida; 8. Colocar uma marca numa casa ocupada; 9. Colocar uma marca numa coluna fora da faixa; 10. Colocar uma marca numa linha fora da faixa; Neste momento, eu tenho certeza que você já tem uma boa noção sobre Desenvolvimento Dirigido por testes. Que tal tentar fazer os dois ultimos testes sozinho? No proximo artigo, veremos: 1. Mais testes unitários (para tornar o nosso JogoDaVelha mais robusto); 2. Como implementar a Interface com o usuário. Veja o código final no Anexo I: Referências: Artigo elaborado e adaptado por Marcos George, baseado em um artigo de Venkat Subramaniam (http://www.agiledeveloper.com/articles/TDDPartI.pdf) Anexo I – código fonte de uTabuleiro.pas
unit uTabuleiro;

interface

uses
  SysUtils, Classes;

type
  EJogoDaVelhaError = class(Exception);

  TTabuleiroJV = class
  private
    FCasas: array [1..3, 1..3] of string;
    FProximoJogadorJogaX: Boolean;
    function GetGameOver: Boolean;
    procedure SetPrimeiroJogadorJogaX(const Value: Boolean);
    function GetPrimeiroJogadorJogaX: Boolean;
  public
    constructor Create;
    procedure MarcarCasa(lin, col: Integer);
    function CasaContemUmX(lin, col: Integer): Boolean;
    property GameOver: Boolean read GetGameOver;
    property PrimeiroJogadorJogaX: Boolean read GetPrimeiroJogadorJogaX write SetPrimeiroJogadorJogaX;
  end;

implementation

uses TestFramework;

{ TTabuleiroJV }

constructor TTabuleiroJV.Create;
begin
  FCasas[1,1] := '';
  FCasas[1,2] := '';
  FCasas[1,3] := '';
  FCasas[2,1] := '';
  FCasas[2,2] := '';
  FCasas[2,3] := '';
  FCasas[3,1] := '';
  FCasas[3,2] := '';
  FCasas[3,3] := '';
end;

function TTabuleiroJV.GetGameOver: Boolean;
begin
  Result := False;
end;

function TTabuleiroJV.GetPrimeiroJogadorJogaX: Boolean;
begin
  Result := FProximoJogadorJogaX;
end;

function TTabuleiroJV.CasaContemUmX(lin, col: Integer): Boolean;
begin
  Result := (FCasas[lin, col] = 'X');
end;

procedure TTabuleiroJV.MarcarCasa(lin, col: Integer);
begin
  if FCasas[lin, col] <> '' then
    raise EJogoDaVelhaError.Create('A casa já está ocupada');

  FCasas[lin, col] := 'O';
  if FProximoJogadorJogaX then
    FCasas[lin, col] := 'X';

  FProximoJogadorJogaX := not FProximoJogadorJogaX;
end;

procedure TTabuleiroJV.SetPrimeiroJogadorJogaX(const Value: Boolean);
begin
  FProximoJogadorJogaX := Value;
end;

// *************************************************
// Esta é a classe que efetuará os testes unitários
// *************************************************
type
  TTabuleiroJVTests = class(TTestCase)
  private
    FTabuleiro: TTabuleiroJV;
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestCriarTabuleiro;
    procedure TestSetPrimeiroJogador;
    procedure TestSetPrimeiroJogadorNovamente;
    procedure TestMarcarAPrimeiraCasa;
    procedure TestMarcarDuasCasasEmSeguida;
    procedure TestMarcarTresCasasEmSeguida;
    procedure TestMarcarUmaCasaOcupada;
  end;

{ TTabuleiroJVTests }

// *****************************************************************************
// Este método é chamado no início de cada teste
// pode ser usado pra configurar o teste, criar variáveis de instância, etc.
// *****************************************************************************
procedure TTabuleiroJVTests.SetUp;
begin
  inherited;
  FTabuleiro := TTabuleiroJV.Create;
end;

// *****************************************************************************
// Este método é chamado no fim de cada teste
// pode ser usado pra liberar os recursos, entre outras coisas
// *****************************************************************************
procedure TTabuleiroJVTests.TearDown;
begin
  inherited;
  FreeAndNil(FTabuleiro);
end;

procedure TTabuleiroJVTests.TestCriarTabuleiro;
begin
  CheckNotNull(FTabuleiro);
  CheckFalse(FTabuleiro.GameOver);
end;

procedure TTabuleiroJVTests.TestSetPrimeiroJogador;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  CheckTrue(FTabuleiro.PrimeiroJogadorJogaX);
end;

procedure TTabuleiroJVTests.TestSetPrimeiroJogadorNovamente;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  CheckTrue(FTabuleiro.PrimeiroJogadorJogaX);
  FTabuleiro.PrimeiroJogadorJogaX := False;
  CheckFalse(FTabuleiro.PrimeiroJogadorJogaX);
end;

procedure TTabuleiroJVTests.TestMarcarAPrimeiraCasa;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(1, 1);
  CheckTrue(FTabuleiro.CasaContemUmX(1, 1));
end;

procedure TTabuleiroJVTests.TestMarcarDuasCasasEmSeguida;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(1, 1);
  CheckTrue(FTabuleiro.CasaContemUmX(1, 1));
  FTabuleiro.MarcarCasa(1, 2);
  CheckFalse(FTabuleiro.CasaContemUmX(1, 2));
end;

procedure TTabuleiroJVTests.TestMarcarTresCasasEmSeguida;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(2, 1);
  CheckTrue(FTabuleiro.CasaContemUmX(2, 1), '1ª casa');
  FTabuleiro.MarcarCasa(2, 2);
  CheckFalse(FTabuleiro.CasaContemUmX(2, 2), '2ª casa');
  FTabuleiro.MarcarCasa(1, 2);
  CheckTrue(FTabuleiro.CasaContemUmX(1, 2), '3ª casa');
end;

procedure TTabuleiroJVTests.TestMarcarUmaCasaOcupada;
begin
  FTabuleiro.PrimeiroJogadorJogaX := True;
  FTabuleiro.MarcarCasa(2, 1);
  // Aki dizemos pro framework que estamos aguardando uma exceção do tipo
  // EJogoDaVelhaError.
  // Se esta exceção não ocorrer, o teste irá falhar
  ExpectedException := EJogoDaVelhaError;
  FTabuleiro.MarcarCasa(2, 1);
  ExpectedException := nil;
end;

initialization
  TestFramework.RegisterTest('TTabuleiro Tests', TTabuleiroJVTests.Suite);

end.

Nenhum comentário:

 

web analytics software