Leia os artigos anteriores
Agrupando casos de teste no SimpleTest
*
Olá, pessoal!
Continuando nossa sequência de artigos sobre o SimpleTest, já aprendemos como fazer testes unitários e agrupa-los para facilitar a execução em lote. Agora vamos falar um pouco sobre mock objects.
Até agora, nossos testes unitários se resumiram a testar métodos e funcionalidades que envolviam apenas uma classe
como nossa calculadora: tínhamos um método chamado “somar” que fazia
parte da própria classe e recebia como parâmetro de entrada apenas dois
inteiros.
A idéia do teste unitário (ou de unidade), como o próprio nome diz, é isolar e testar apenas aquele ponto e funcionalidade
do software: então é imprescindível que possamos isolar da melhor
maneira apenas a classe/métodos que desejamos testar para que, caso
apresente falha, possamos identificar exatamente onde a falha ocorreu.
Mas e quando nossa classe depende de outra(s) classe(s) para fazer um método funcionar?
Conceituando o problema
Então, vamos imaginar a seguinte situação: estamos desenvolvendo um
software de e-commerce e em dado momento precisamos validar se um
usuário existe em nosso banco de dados. Apesar das possibilidades
abrangentes de um sistema de validação/autenticação, vamos nos
preocupar com o básico e implementá-lo de uma forma simples como abaixo:
As classes usadas no validador
Vamos criar uma classe chamada Validator,
que receberá o login do usuário, verificará no banco de dados se ele
existe ou não e validará seu acesso. Para tanto, nossa classe de
validação terá um método chamado validate, que receberá um parâmetro string representando o nome do usuário a ser validado.
Para conectar-se ao banco de dados, nossa classe receberá em sua criação um objeto referente à classe Conexao, que disponibilizará um método chamado query, que consulta o banco e retorna uma array de resultados.
classe validator.php
class Validator {
var $conexao;
function __construct($conexao) {
$this->conexao = $conexao;
}
public function validate($nome) {
// aqui vai a lógica para verificar se o usuário existe ou não e que vamos testar
}
}
classe conexao.php
class Conexao {
function __construct($config) {
// configura o acesso ao banco de dados
}
function query($query) {
// retorna um array com os resultados da consulta
}
}
Para que tudo fique mais claro, podemos dar uma olhada em como ficaria nosso código final da aplicação:
$config = new Config();
$conexao = new $Conexao($config);
$validator = new Validator($conexao);
$usuario = 'Léo';
if ($validator($usuario)) {
// redireciona para a página restrita
} else {
// retorna uma alerta falando que o usuário não existe
}
Como aprendemos em nosso primeiro artigo sobre TDD, primeiro fazemos os testes e depois fazemos nossa aplicação passar nos neles, implementando a lógica de negócio válida. Nosso objetivo nesse momento é testar o método validate: fazendo um pouco de nossa flexão mental chegamos a dois comportamentos válidos para o método:
- Se o usuário existir, ele retornará true;
- Se o usuário não existir, ele retornará false;
O problema
Então, pensamos “Bacana! Então vou fazer um caso de teste
chamado ValidatorTestCase e um teste chamado
testValidarSeUsuarioExistir que irá validar se um usuário foi validado
se ele existir!“.
Partimos para o código então:
class ValidatorTestCase extends UnitTestCase {
function testValidarSeUsuarioExistir() {
$config = new Config();
$conexao = new Conexao($config);
$validator = new Validator($conexao);
$this->assertEqual($validator->validate('Léo'), true);
}
}
Tudo ok? NADA OK! O código acima pode funcionar mas temos alguns problemas:
- Como garantir que nosso banco de dados vai estar disponível na hora do teste?
- Como vamos saber se existe o usuário que queremos testar está no
banco, para testarmos se a validação realmente vai passar no teste e se
comportar como queremos? - E se ocorrer um erro de conexão ao banco no meio do teste?
- E se a conexão banco for demorada e nosso caso de teste ter 50 testes acessando o banco? oO
- Como saber se as configurações estão exatas para a conexão no banco? Aliás, eu nem sei nada sobre config até agora!
- E principalmente: e se ocorrer um problema na pesquisa da query que
não tenha nada haver exatamente com nosso validador, já que nosso
objetivo é testar unicamente se “ao receber um usuário, verificar se
ele existe no banco e validar em caso afirmativo”.
Haja coisa né?
Mock Objects (nosso boneco de testes)
Pensando nesse problema, foram criados os Mock objects. Um mock object (vou usar o termo em inglês pois a esmagadora maioria das referências TDD estão assim) nada mais é que um objeto que simula e imita o comportamento de um objeto real.
Imagine os bonecos de teste de batidas de carro: os engenheiros os constroem com peso, altura, juntas e vários aspectos que imitam o corpo de um ser humano para poderem fazer seus testes.
Eles colocam esses bonecos nas mais variadas situações e posições
possíveis para testarem a segurança de um carro entre outras coisas,
sem precisar matar ninguém para isso.
No nosso caso, nossos mock objects são representações de
classes reais onde podemos simular e forçar qualquer comportamento que
desejamos para testarmos certas situações que talvez não
seriam possíveis de serem alcançadas “explicitamente” sem gambiarra-las
ou mesmo que sejam de dificil configuração.
Em resumo, usamos um mock quando nossa classe real (colado do wikipedia? thank’s god):
- gera resultados não determinísticos (e.g. a hora ou temperatura atual);
- tem estados que são difíceis de criar ou reproduzir (e.g. erro de comunicação da rede);
- é lento (e.g. um banco de dados completo que precisa ser inicializado antes do teste);
- ainda não existe ou pode ter comportamento alterado;
- teriam que adicionar informações e métodos exclusivamente para os testes (e não para sua função real).
Analisando nosso problema anterior, podemos constatar que nossa classe Conexao
é forte candidata à ser “mockeada”: assim nos livramos de todos aqueles
problemas de conexão, de existência de registros e tudo mais que não
tem nada haver com nosso teste unitário em si, que diz respeito pura e
diretamente à nossa classe Validator.
Ou seja, ao invés de usar a classe Conexao real, vamos usar uma “imitação” dela, muito mais simples e fácil de manipular
para chegarmos de forma mais simples ao nosso resultado esperado no
teste unitário. Vamos utilizar esse mock pois não faz sentido testar a
conexão nem nenhum método dessa classe e sim utilizar o comportamento
referente ao código que vamos utilizar no nosso teste.
Codando!
Como vimos, nossa classe Conexao
não tem nada implementado ainda. Mas isso não nos interessa pois vamos
fazer um mock da classe, para podermos simular o que precisamos: o
retorno dos resultados que desejamos para testar o método validate do Validator.
Para criar um mock de nossa classe, simplesmente fazemos o include dessa classe em nosso teste e “geramos” esse mock com o método generate da classe Mock (que é importada automaticamente no nosso include do autorun.php do Simpletest).
require_once('simpletest/autorun.php');
require_once('conexao.php');
require_once('validator.php');
Mock::generate('Conexao');
Com isso, vamos gerar um clone mockeado da classe Conexao para podermos usa-la em nossos testes.
require_once('simpletest/autorun.php');
require_once('conexao.php');
require_once('validator.php');
Mock::generate('Conexao');
class ValidatorTestCase extends UnitTestCase {
function testValidarSeUsuarioExistir() {
$conexao = new MockConexao();
...
...
}
}
Fácil né?
Mocks como atores
Agora que sabemos como criar um mock, vamos aprender a “imitar” o que a classe original faz afim de “simular” os resultados que queremos ter para fazer nossos testes passarem.
Os Mocks podem se comportar de duas maneiras:
- como Atores (Actors), onde o Mock é usado para simular o retorno de propriedades e métodos (e o que vamos usar aqui).
- como Críticos (Critics), onde o Mock é usado para guardar e verificar as interações entre os objetos num teste.
Quando trabalhamos com o Mock apenas como Ator, apenas
simulando o retorno de propriedades e métodos, ele é considerado um
“server stub”, que nada mais é que um objeto que simula um
comportamento. Existe uma grande discussão existe em torno disso, mas
por enquanto vamos deixar pra lá pois vamos falar sobre os Mocks como
uma ferramenta para testes baseados em interação no próximo post e isso
vai ficar mais claro.
Se analisarmos a classe Conexao novamente, vamos chegar à conclusão que o método que vai ser utilizado em nosso teste será apenas o query: vamos fazer uma consulta à base dados buscando todos os usuários cujo nome seja igual ao parametro que enviarmos ao método validate.
Nosso mock da Conexao ainda não sabe fazer nada, mas podemos instrui-lo a retornar o que a quisermos. Por exemplo, em nosso teste chamado testValidarSeUsuarioExistir
vamos ter que dizer ao mock para retornar sempre um resultado que
satisfaça nosso teste. Ou seja, como no exemplo da implementação, se eu
envio “Léo” ele tem que me devolver um resultado onde “Léo” esteja
presente, pois queremos testar o comportamento do validate e não o retorno de valor da conexão.
Então, vamos fazer nosso mock devolver sempre um resultado onde exista o usuário “Léo” no resultado.
Como vimos, nosso método query
retorna uma array com os resultados da consulta. Vamos então instruir o
mock a retornar sempre um array quando o método query for chamado.
Quando o objeto é mockeado, ele ganha vários métodos, entre eles o setReturnValue: esse método diz ao mock “retorne sempre X resultado quando tal método for chamado”.
$conexao=new MockConexao();
$conexao->setReturnValue('query',array('1','Léo'));
Pronto! Agora podemos testar nosso validate:
require_once('simpletest/autorun.php');
require_once('conexao.php');
require_once('validator.php');
Mock::generate('Conexao');
class ValidatorTestCase extends UnitTestCase {
function testValidarSeUsuarioExistir() {
$conexao = new MockConexao();
$conexao->setReturnValue('query',array('1','Léo'));
$validator = new Validator($conexao);
$this->assertEqual($validator->validate('Léo'), true);
}
}
O que aconteceu em nosso teste:
- Criamos um mock do nossa classe Conexao
- Instanciamos esse mock em nosso teste
- Instruimos ao mock para que ele devolvesse sempre um array com o usuário Léo quando o método query fosse chamado
- Instanciamos nosso validator, passando como parâmetro nossa conexao mockeada
- fizemos o teste para verificar se a validação deu certo
Fui rodá-lo e?
Opsss! Falhou!
Temos nosso resultado esperado, afinal não implementamos ainda nosso método validate. Então, vamos resolver isso:
class Validator {
var $conexao;
function __construct($conexao) {
$this->conexao = $conexao;
}
public function validate($nome) {
$resultados = $this->conexao->query("select id, nome from users where nome = '{$nome}'");
if (array_search($nome,$resultados)) {
return true;
}
return false;
}
}
Implementamos uma lógica onde fazemos uma pesquisa ao banco pedindo
para selecionar o id e o nome da tabela users onde o nome seja igual ao
parâmetro enviado. A mágica acontece finalmente: nossa ” falsa conexao” foi instruída a retornar sempre um array com uma posição apenas, onde o resultado sempre é Léo.
Agora quando rodamos o teste:
Teste do validator agora com a implementação
Bingo! Nosso teste passou!
Para comprovarmos que o funcionamento do validate
está correto, vamos complementar nosso caso de teste adicionando um
teste pra verificar se informarmos um usuário que não existe, ele
retornar false.
require_once('simpletest/autorun.php');
require_once('conexao.php');
require_once('validator.php');
Mock::generate('Conexao');
class ValidatorTestCase extends UnitTestCase {
function testValidarSeUsuarioExistir() {
$conexao = new MockConexao();
$conexao->setReturnValue('query',array('1','Léo'));
$validator = new Validator($conexao);
$this->assertEqual($validator->validate('Léo'), true);
}
function testNaoValidarSeUsuarioNaoExistir() {
$conexao = new MockConexao();
$conexao->setReturnValue('query',array('1','Léo'));
$validator = new Validator($conexao);
$this->assertEqual($validator->validate('Jeveaux'), false);
}
}
Vamos testar de novo e?
Fazendo dois testes
Mantemos o nosso mock retornando apenas “Léo” nos resultados, mas
dessa vez passamos o nome “Jeveaux” para procurar e, como esperado, ele
retornou false, dado que o usuário “Jeveaux” não existe. Show!
Ufa! Vamos resumir o testamento
O aprendizado de mock objects é um pouco abstrato inicialmente mas
com alguma prática se revela uma técnica perfeita e viável para fazemos
nossos testes unitários funcionarem de forma correta e independente. A
utilização do Mock Object apenas como ator (ou server stub como você
lerá muito por ai) é apenas uma parte do poder real de um mock.
Dentro de nosso entendimento, podemos resumir que um
mock object é uma imitação de nosso objeto real que podemos manipular
de forma fácil e simples para que ele faça exatamente o que quisermos,
facilitando assim a implementação de nossos testes unitários e isolando
nossas classes.
Algumas leituras legais melhorar o entendimento dos Mock Objects:
- Mock Objects no SimpleTest [ doc oficial do SimpleTest ]
- Testes de unidade com mock objects [ ImproveIt ]
- Mock Objects and Stubs: The Bottle Brush of TDD
- Mock Objects [ Wikipedia]
Outra referência bem bacana é o blog do Jeveaux, meu revisor TDD oficial e grande conhecer de causa.
No próximo artigo vamos ver como trabalhar com nosso Mock Object em
sua forma mais plena e com um exemplo um pouco mais complexo para
partir também para o modo Critic.
Simbora!