Iniciando com o SimpleTest

Salve people,

Vamos iniciar hoje uma pequena jornada à terras que possivelmente muita gente só viu/leu em sites especializados e muito pouco comentadas em PHP: a terra do desenvolvimento orientado a testes, ou TDD.

Se você não sabe ou nem faz idéia do que é TDD, dê uma procura no Google pois existem dezenas de sites muito bacanas destilando idéias e tudo mais sobre isso.

Em poucas palavras, TDD (Test Driven Development) é um técnica de desenvolvimento de software que nos diz que devemos escrever os testes antes de escrever o código da aplicação propriamente dito.

Inicialmente isso parece meio louco: afinal, você sempre testa DEPOIS de escrever seus programas ou durante, enquanto debuga tudo, correto ? Mas com o passar do tempo, a verdadeira natureza e vantagem do TDD, quando aplicada corretamente, se faz presente.

Você se torna capaz de antecipar a detecção e correção de várias falhas, reduzindo dramaticamente o tempo gasto com correções em cima de implementações muito complexas já praticamente no final do seu cronograma.

Hoje em dia, existem várias frameworks que auxiliam nessa tarefa de escrever testes. O foco aqui é algo que poucos sites (principalmente em português) abordam de forma prática que é o uso da SimpleTest, uma framework para testes unitários que vem ganhando o espaço antes ocupado pelo PHPUnit.

No decorrer dos próximos posts sobre o SimpleTest, você poderá adquirir um pouco de conhecimento que poderá ser útil em seus futuros projetos. Então, vamos simbora.

Instalando o SimpleTest

A instalação do SimpleTest em si é muito fácil.

  1. Baixe a versão mais atual do SimpleTest no site http://www.simpletest.org (a versão que usaremos nessa sequência de tutoriais é a 1.0.1). A framework é composta por uma pasta simples;
  2. Descompacte o arquivo dentro de sua aplicação. Para fins de organização, vamos criar uma pasta “tests” na raíz de nossa aplicação e descompactar o zip/tar do SimpleTest lá: ao descompactar você verá uma pasta chamada simpletest sendo criada.

Em tese, nossa aplicação pode ter qualquer estrutura de diretórios. O SimpleTest funciona tanto com functions como com classes. Vamos abordar o uso de classes, dado que o TDD é amplamente usado em sua maioria em soluções OO (Orientadas a Objeto) e acho que já passou da hora da comunidade PHP pensar OO. :D

Partindo desse pressuposto, vamos criar a pasta “classes” na raíz de nossa aplicação: lá iremos botar todas as nossas classes que serão usadas nos testes.

Teremos então, uma estrutura de arquivos como abaixo:

imagem5

A pasta app_tdd é a pasta onde está nossa aplicação: uma pasta criada dentro do meu htdocs (raíz do Apache).

Se o seu servidor web estiver instalado com configurações padrão, provavelmente você poderá acessar usando: http://localhost/app_tdd

Nosso problema: uma calculadora

Com o SimpleTest “instalado” em nossa aplicação e nossa estrutura de diretórios resolvida, vamos escrever nosso primeiro caso de teste.

O cenário de nossa aplicação é uma calculadora: nossa calculadora conseguirá efetuar apenas a operação de soma.

Com uma análise rápida do problema, já nos vem à cabeça que:

  • Uma classe chamada “Calculadora” com um método chamado “soma”;
  • Nosso método soma recebendo dois números
  • Nosso método retornando o valor da soma entre os dois números

Nossa abordagem não TDD seria: vamos fazer a classe, implementar o método e depois testa-lo em uma página teste. Correto ? Num primeiro momento isso seria ótimo: afinal, o código e complexidade das classes inicialmente são lindos.

Mas imagine agora sua aplicação crescendo e crescendo: classes extendendo e usando outras classes. Você extende a Calculadora, outra classe utiliza o método soma e você vai testando apenas “o que vem depois”.

Num dado momento, você tem um resultado incorreto de soma: uma entrada incorreta de parâmetros, um deles ser uma letra e não um número, termos uma passagem de um objeto ao invés de um número propriamente dito … De quem é a culpa ? Da classe nova, que esqueceu de filtrar a entrada de parametros ? Do designer, que esqueceu de limitar a entrada dos valores no form para apenas números ? Do outro programador, que foi descuidado e não validou se os dados passados eram realmente números antes de chamar a soma ?

Enfim, temos N cenários onde a detecção do erro pode ser muito custosa, seja pelo método para encontra-lo (que varia do debug minucioso ao “achismo”) e/ou pelo custo em termos de tempo para concerta-lo. Tudo isso pode gerar um custo/prejuízo que seria reduzido com a implementação do pensando TDD.

Pensando primeiro em testes

Mentalize: “Quais as situações que podem quebrar meu método soma? Se acontecer, como devo tratar esse erro ?”

Com base nesse pensamento, podemos deduzir:

  • Para somar, nossa calculadora terá que receber sempre dois números;
  • A soma sempre ocorre entre dois números, nunca entre letras, objetos ou qualquer coisa que não seja exatamente um número;
  • Se algo der erro, devo retornar falso;

Interessante! Não implementamos nenhuma linha de nossa solução e já sabemos:

  • Que vamos precisar de uma classe (Calculadora) com um método de soma;
  • Sabemos que o método deverá receber dois parâmetros que deverão ser sempre números;
  • Que se for passado qualquer coisa que não sejam dois números, eu devo retornar falso;

Bom, então vamos implementar a classe ? Não, pequeno gafanhoto: vamos implementar primeiro os testes, porque é com base neles que vamos ter certeza que nossa classe se comportará exatamente como pensamos que ela deve se comportar sob os mais diversos cenários.

Escrevendo nosso primeiro teste

Implementar um teste com SimpleTest é, como o nome já diz, “simples”.

Vamos criar todos os nossos testes dentro da pasta tests. Para cada classe que tivermos que testar, vamos criar um caso de teste (unit test case) que será representado em um arquivo php.

Então, nosso primeiro caso de teste será o calculadora_test.php. O nome do arquivo não tem um padrão de nomenclatura, mas por convenção usa-se sempre nomedaclasse_test.php.

calculadora.php -> calculadora_test.php

A estrutura inicial do nosso arquivo calculadora_test.php será a seguinte:

1
2
3
4
5
6
require_once('simpletest/autorun.php');
require_once('../classes/calculadora.php');

class TestOfCalculadora extends UnitTestCase {
// os testes vão aqui ;)
}

Pronto! Nosso caso de teste da classe Calculadora está feito. Para testa-lo, vamos apontar o browser para http://localhost/app_tdd/tests/calculadora_test.php. O resultado será:

1
2
3
Warning: require_once(../classes/calculadora.php) [function.require-once]: failed to open stream: No such file or directory in /Applications/MAMP/htdocs/app_tdd/tests/calculadora_test.php on line 4

Fatal error: require_once() [function.require]: Failed opening required '../classes/calculadora.php' (include_path='.:/Applications/MAMP/bin/php5/lib/php:/Users/leohackin/PEAR') in /Applications/MAMP/htdocs/app_tdd/tests/calculadora_test.php on line 4

Ops! Não criamos nossa classe ainda, por isso obtemos esse erro. Quando disse que escrevemos testes antes de implementar nossa lógica, estava falando sério. =)

Vamos criar nossa classe Calculadora então.

1
2
3
4
class Calculadora {
function soma($a,$b) {
}
}

Legal, agora vamos acessar nosso caso de teste denovo.

imagem6

Uhu! Funfou!!!

Analisando o código:

  • fazemos o include de dois arquivos:
    • o arquivo autorun.php é o arquivo que faz a “mágica” acontecer: é ele quem roda os testes e exibe os resultados, portanto deverá sempre estar no seu caso de teste;
    • o outro arquivo é a classe que desejamos usar no teste, no caso calculadora.php
  • Criamos uma classe chamada TestOfCalculadora extendendo UnitTestCase, que será a classe que o SimpleTest usará para fazer o teste. É obrigatório que a classe inicie com o nome “test” para que o SimpleTest execute automaticamente a mesma como um caso de teste. Existe uma forma de faze-lo sem iniciar o nome com “test”, mas isso não vem ao caso agora.

Maneiro né ? Mas como puderam notar, nosso caso de teste não testa nada ainda. Hahahah

Vamos adicionar agora um teste: o teste vai verificar se a soma está realmente “somando” dois números. Para isso, devemos adicionar um método à nossa classe de testes. Vamos chamar esse teste de “testSomaDoisNumerosInteiros“, onde vamos passar dois números inteiros esperando que a soma deles esteja correta.

Usar nomes grandes assim no nome do método são uma boa prática, já que deixam os testes mais legíveis na hora de rodar o caso de teste.

1
2
3
4
5
6
7
8
9
10
11
require_once('simpletest/autorun.php');
require_once('../classes/calculadora.php');

class TestOfCalculadora extends UnitTestCase {

function testSomaDoisNumerosInteiros() {
$calculadora = new Calculadora();
$this->assertEqual($calculadora->soma(1,1), 2);
}

}

Como visto, temos nosso método testSomaDoisNumerosInteiros que instancia nossa classe Calculadora e depois executa um método chamado assertEqual. Esse método é o responsável por testar nossa soma. Ele significa:

Verifique se a chamada $calculadora->soma(1,1) retornará um resultado igual à 2

Se a chamada retornar qualquer coisa diferente de dois, nosso teste irá falhar, indentificando que algo de podre está acontecendo em nosso método soma.

Se rodarmos esse script teremos enfim:

imagem1

Tivemos uma falha. Traduzindo a mensagem de forma prática:

O seu teste testSomeDoisNumerosInteiros, do caso de teste TestOfCalculadora, falhou porque NULL (que foi retornado pela chamada ao nosso método soma) não é igual a 2 (que seria nossa resposta esperada).

A resposta para isso é que ainda nem implementamos nosso método soma. Mas vejamos que nesse ponto já sabemos exatamente como deve ser comportar nosso método para o funcionamento com dois números. :)

Vamos implementar nossa classe então:

1
2
3
4
5
class Calculadora {
function soma($a,$b) {
return $a + $b;
}
}

Com nosso método agora implementado, vamos executar nosso caso de teste denovo.

imagem2

Agora sim! Temos um caso de teste funcional que testa uma classe implementada. Parabéns por chegar até aqui.

Nesse ponto, já temos conhecimento suficiente para escrever vários casos de teste para nossas classes. Um caso de teste pode conter vários testes diferentes: cada teste é feito através de um método da classe do caso de teste.

Revisando aquelas possibilidades de cenário que poderiam “quebrar” nossa calculadora, já testamos se a soma está correta. Agora, podemos testar as possibilidades que podem gerar um erro na calculadora.

Uma delas é se passarmos letras no lugar de números: haviamos combinado que nessa situação, devolveriamos falso para o resultado, correto ? Então, vamos escrever o teste: vamos chama-lo de “testSomaNaoNumeros“:

1
2
3
4
function testSomaNaoNumeros() {
$calculadora = new Calculadora();
$this->assertEqual($calculadora->soma(1,'A'), false);
}

Adicionamos essa função à nossa classe. Rodamos nosso teste novamente e …

imagem3

Previsivelmente, temos um erro pois nosso método soma ainda não verifica se os parâmetros recebidos são números válidos. Ai você irá pensar:

Mas eu vou escrevendo os testes e vou implementando toda a minha lógica de negócio ao mesmo tempo ?

A TDD tem uma característica bacana, que anda de mãos dados ao refactoring: a TDD nos diz que devemos SIM escrever os testes primeiro e fazer as classes “passarem no teste” usando o mínimo de código possível: se a lógica for complexa, retorne uma resposta “hardcode” para “enganar” o teste e depois faça o refactory do código.

O refactory deve ser feito apenas depois de todos os testes serem feitos, pois nesse ponto você terá certeza de como o funcionamnento de sua classe atenderá a todos as respostas que são requisitadas nos testes como “corretos”.

Pensando nisso, vamos fazer nosso método soma “passar” no teste:

1
2
3
4
5
6
7
8
9
class Calculadora {
function soma($a,$b) {
if (is_int($a) && is_int($b)){
return $a + $b;
} else {
return false;
}
}
}

Agora, vamos rodar nosso teste.

imagem4

Blz! Nosso teste passou, mas testamos apenas se os valores são inteiros e se forem, efetuamos a soma. Se não forem, a gente retorna false, como nosso teste pediu. Podemos depois refatorar isso: verificar se o valor é uma string com um número dentro e por ai vai.

Finalizando

Você pode estar se perguntando: “Uai, mas podemos ter muito mais ocasiões que podem quebrar a soma! Podemos também extender algumas funcionalidades e exibir mensagens de erro”.

Tivemos uma amostra do que é o SimpleTest em seu cenário mais simples: apesar do tamanho do post, o conceito e a aplicação são bem simples como puderam ver.

Além do assetEqual, a SimpleTest tem um set de ações enorme de validações, além de recursos mais avançados, como suites, mocks e web tests que veremos em breve.

Crie outras classes, pense nos testes, escreva seus casos de teste e vá executando: com a prática isso vai ficar tão automático que o ganho com a diminuição dos testes e bugs no final da aplicação vão ser notórios.

Testes nos tornam programadores melhores. Pense nisso.

Algumas coisas para se pensar quando começar a abordar isso:

  • Não precisamos escrever TODOS os testes: é completamente normal se esquecermos algo ou houver alguma necessidade de mudança de negócio do cliente que nos fará escrever novos testes ou re-escrever os existentes. Tenha em mente que o TDD é para ajudar e não para ser mais uma fase carrancuda e intransponível no desenvolvimento;
  • A análise para chegar aos casos de teste faz bem ao início do projeto: com essa abordagem, você pode fazer perguntas ao cliente (e ele a você) sobre algumas coisas que possívelmente só apareceriam no final do projeto gerando assim muito re-trabalho;

Bom, por enquanto é isso pessoal. No próximo post falaremos um pouco sobre agrupamentos de teste e partir para um exemplo mais complexo. :)

Espero que tenham gostado do post.

Simbora! :D

  • http://www.jeveaux.com Jeveaux

    Excelente post, papai! Didática nota 10!

    (você deve dar fácil pra um bom professor hein heeheh)

  • http://www.leohackin.com.br/blog Léo Hackin

    Yeah! Valew! E nada de dar pra ninguém. ahahahahah

  • http://blog.will.eti.br William G. Comnisky (PHPSC)

    Saudações Leo, parabéns pelo excelente artigo. Eu acho válido criar dois testes logo no início: o primeiro pra verificar se a classe existe, e outro pra verificar se o método existe.

    Também pra facilitar a vida do desenvolvedor, é interessante comentar que existem plugins para quem utiliza o Eclipse (e Netbeans também), podendo – através de algumas teclas de atalho – ver rapidamente a execução e resultado dos testes.

    abraço,

  • http://www.leohackin.com.br/blog Léo Hackin

    Salve Willian,

    Valew pelas palavras cara. Observação muito bacana sobre os plugins mesmo: como nesse inicio a idéia é mostra a SimpleTest em si independente de plataforma, vou anotar pra falar sobre isso lahhhhh no final. =)

    Sobre a verificação da existência de classe: partindo do pressuposto que você cria um teste para usar uma classe e dado método, se você faz essa verificação antes você vai estar testando “duas vezes” a mesma coisa, já que se a classe ou método não existirem, o assert vai falhar concorda ? Para o teste passar você tem que estar com tudo certin, o que vai dispensar essa verificação e fazer você passar assim “O que importa é passar no teste. O que deu errado que o meu teste não passou?” :)

    Abração e valew pelo comentário.

  • http://www.comocriarsites.com.br Natanael

    Muito bem explicado, excelente. Parabéns.

  • Pingback: O melhor da semana 28/06 a 04/07 « QualidadeBR

  • http://blog.will.eti.br William G. Comnisky

    Saudações Leo! importante fazer a distinção entre falha e erro.

    Se você utiliza um file_exists() ou class_exists() em seus testes e o arquivo ou a classe não existir, você terá uma falha; senão, você terá um erro ‘cru’ do PHP.

    Acho que o que realmente importa é passar corretamente nos testes, tendo todos possíveis erros e exceções tratados. E quando você achar um erro ou exceção não tratado, faça o teste para o mesmo..

    abraço!

  • http://www.leohackin.com.br/blog Léo Hackin

    Salve Willian,

    Ah sim! Testes para esse tipo de verificação sim. ;) Legal essa explicação de distinção que você deu. Acho que interpretei mal o que você havia dito.

    Abração chefe.

  • Pingback: PHP: Iniciando com o SimpleTest - TDD

  • http://hcalves.tumblr.com Henrique

    Maravilhoso saber que há TDD no mundo PHP, parabéns por divulgar.

    Agora… meio bizarro acessar o teste via browser, não? Não existe como rodar via shell?

  • http://www.keepgeek.com.br André Machado

    Muito interessante o artigo. Vou começar a implementar isso em minhas aplicações. Você está de parabéns.

  • Tarcisio

    Excelente artigo. Qual foi o programa que utilizou para escrever o php. A interface é zebrada é muito show bola. Que plug in ou programa seria esse, amigão? Abs.

  • http://www.leohackin.com.br/blog Léo Hackin

    Valew pelas palavras pessoal. :)

    Tarcísio, esse pluguin do WordPress é o Developer Formatter. Bem bacana.

  • Tarcisio

    Obrigado pela dica. Abraços.

  • http://www.pequenotux.blogspot.com Willian

    Boa tarde, artigo muito bom. Eu tenho uma dúvida. Por exemplo, se meu método não retornar nada por exemplo, ou se meu método é usado apenas para criar uma imagem, por exemplo, como eu faria para testar este método?
    Obrigado, um abraço.

  • http://www.leohackin.com.br Léo Hackin

    Salve Willian, valew pelas palavras.

    O SimpleTest tem vários tipos de asserts para chegar o retorno de um método no teste unitário: por exemplo, se vc usa um assertEqual você diz ao SimpleTest que tal método vai falhar se o resultado retornado não for “igual” ao que você diz que tem que retornar. Vamos supor que você chame o método soma passando dois parametros (1 e 1) e fale que o retorno tem que ser igual a 2. Se retorno for vazio, então seu método falhou. ;) O lance do TDD é “dedurar” essas falhas em comportamentos esperados.

    No caso de um método que retorna imagens, é o caminho da imagem ou o hash binário dela ?

    Abraço.

  • http://www.pequenotux.blogspot.com Willian

    Boa tarde, obrigado pelo retorno.
    Na verdade no exemplo da imagem não retornaria nada. Seria uma função que apenas executasse o processamento da imagem mas não retornasse nada. Como eu poderia saber se esse processamento funcionou? TDD só testa valores de retorno?
    Um abraço.

  • http://www.leohackin.com.br/blog Léo Hackin

    Salve Willian,

    Se o método não retorna nada ele retorna vazio correto ? Basta testar se ele retorna vazio com um assertEqual ou assertIdentical. ;)

  • http://www.pequenotux.blogspot.com Willian

    Então o desenvolvimento baseado em testes avalia apenas o retorno das funções :)
    Não existe nada para avaliar um processamento?
    Obrigado e desculpa pela ignorância… Isso é novidade pra mim…

  • http://www.leohackin.com.br Léo Hackin

    O que rola é o seguinte Willian: o que você tem que saber para avaliar se um “processamento” fez o que você gostaria ou não ? Independente do processamento, ele sempre vai fazer um resultado correto ? Um método de gerar imagem retorna um hash binário da imagem ? Um método de gerar sessão cria uma variável na sessão ? São exemplos que não retornam nada mas tem um comportamento que gera algum resultado: é esse resultado que você testa com um caso de teste.

    Se o processamento gera uma imagem física no HD, faça um método que verifica se após o método criarImagem (por exemplo) gerou a imagem esperada no lugar esperado.

    Um teste não avalia o processamento nem retorno de funções: um teste avalia se um método (ou objeto) sob tal circunstância se comporta da forma que você esperava. Isso você faz através dos asserts, entendeu ?

    No próprio site do SimpleTest existe um exemplo bacana de teste de uma classe que trabalha com log que de certa forma se aproxima desse questionamento.

    Abraço.

  • http://deviupi.com.br Cristiano

    Leo, Parabéns pelo post! Vou tentar implementar em alguns aplicativos aqui na empresa, já tinha escutado falar, mas não pensei que teria uma forma tão Simples para isso…vlww e parabéns!! continue com os posts sobre TDD.

  • http://wolferineblues.com wolf3rin3.blu35

    Sempre procuro esgotar os testes mas ainda não tinha pensado em um padrão de nomenclatura para os métodos e classes organizados da forma proposta.

    Meus parabéns pelo post!

  • Pingback: Mock Objects no SimpleTest | Léo Hackin 0.2c

  • Wilson

    Não consigo ver os códigos porque tem o erro: GeSHi could not find the language phplines (using path /home/leohackin/www/wp-content/plugins/codecolorer/lib/geshi/) (code 2)
    :(

  • http://www.leohackin.com.br Léo Hackin

    Corrigido meu chefe! Valew pelo toque. ;)

  • Makoto

    Caramba pra que usar esta mer.. de fonte no site. Tá horrivel e péssimo de ler o conteúdo é bom mas a fonte usada muito ruim

  • Luis Milanese

    Excelente post.

  • Ilton Barbosa

    Baxiei e descompactei o arquivo do SimpleTest e não vi o tal arquivo chamado autorun.php

  • Pingback: Mauro George

  • Pingback: RADAGA Proc. Dados

  • Pingback: Dodilei

  • Pingback: Lara

  • Pingback: Osvaldo Zonetti