Introdução ao desenvolvimento guiado por testes (TDD)
Thalita Marra
Posted on June 14, 2021
Olá olá!
Essa é a última postagem da série de Introdução a Testes de Software e quero terminar plantando a sementinha do TDD no arsenal de vocês 🌱
O que é TDD?
TDD é uma sigla para Test Driven Development (ou Desenvolvimento Guiado por Testes) que na prática significa exatamente o que o nome diz: primeiro escrevemos os testes, depois o código de produção.
Colocar o TDD em prática significa trabalhar de forma cíclica: primeiro criamos os testes necessários, que irão falhar; depois vamos escrever o mínimo possível de código para os testes passarem; por fim, se necessário, refatoramos o que foi feito. Bem simples!
A princípio pode parecer meio complexo, mas vamos ver um exemplo prático e vai ficar mais claro uma vez que chegarmos lá.
Modelo F.I.R.S.T.
Quando seguimos o ciclo do TDD é normal ter uma quantidade considerável de testes cobrindo praticamente todo o nosso código de produção.
Chega um momento em que o tamanho dos testes acompanha o tamanho do código de produção, e isso pode se tornar um problema de gerenciamento assustador. Sendo assim, é necessário tratar os testes com o mesmo cuidado que o código de produção.
O modelo F.I.R.S.T. existe para ajudar nesse processo e cada letra do acrônimo representa um princípio a ser seguido:
- Fast (rapidez): os testes devem ser rápidos. Quando eles são lentos se torna tentador não executá-los com frequência, trazendo problemas como encontrar um bug tardiamente, quando for mais difícil de concertá-lo ou quando ele já afetar outras áreas.
- Isolates (isolamento): um teste não deve depender de outro, eles precisam ser executados de forma independente e em qualquer ordem. Dessa forma fica fácil de encontrar onde e porque alguma falha acontece.
- Repeateble (repetitividade): devemos ser capaz de executar os testes unitários em qualquer ambiente, com resultados e comportamentos constantes. Eles não devem depender de algum estado prévio (de um banco de dados já preenchido, por exemplo) e devem deixar o ambiente tal qual estava antes de serem iniciados. Devem rodar com ou sem internet, no seu ambiente local ou no ambiente de produção. Se um teste não roda fora de um ambiente específico, eles não vão te ajudar em todas as situações que poderiam.
- Self-validating (autovalidação): os testes devem resultar em sucesso ou erro, eles não podem depender de alguém validando seu resultado manualmente. Um resultado de erro ou sucesso não deve ser subjetivo.
- Timely (pontualidade): os testes precisam ser escritos no momento certo. No TDD é importante que o teste seja feito antes do desenvolvimento do código de produção. Deixar o teste para depois pode fazer com que eles sejam ignorados ou que não cubram quantos casos poderiam se o fluxo correto fosse seguido.
Exemplo prático
Precisamos desenvolver um sistema de biblioteca e vamos começar pelo cadastro dos livros. Os requisitos são:
01: A biblioteca deve possuir livros
02: Deve ser possível navegar por todos os livros disponíveis na biblioteca
Então seguindo o ciclo do TDD, começamos escrevendo um teste de falha:
01.01. Criar um teste que falhe (vermelho)
Vamos começar a descrever nossa regra de negócio através do teste:
public function testBibliotecaPossuiLivros(): void
{
$book = new Book('Aqueles olhos verdes', 'Pedro Bandeira');
$library = new Library();
self::assertEmpty($library->nextBook());
$library->add($book);
self::assertEquals($book, $library->nextBook());
self::assertCount(1, $library);
}
Quando o PHPUnit executar nosso teste iremos receber um erro:
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 00:00.064, Memory: 4.00 MB
There was 1 error:
1) Tests\BooksTest::testBibliotecaPossuiLivros
Error: Class "Tests\Book" not found
/app/tests/BooksTest.php:13
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
Como a única coisa que fizemos até então foi criar o teste, precisamos criar as classes e métodos necessários para fazer o teste passar.
01.02. Fazer os testes passarem (verde)
Começamos criando a classe Book
e como a única coisa que precisamos nesse momento é a classe com o construtor, então ficou algo bem genérico:
<?php
namespace App;
class Book
{
private string $name;
private string $author;
public function __construct(string $name, string $author)
{
$this->name = $name;
$this->author = $author;
}
}
Já a classe Library
precisa do método count()
para passar no assertCount
. Vamos implementar a interface nativa do PHP Countable
para resolver essa etapa. Também precisamos do método add()
para salvar o livro na biblioteca e do método nextBook()
para iterar sobre os livros disponíveis.
<?php
namespace App;
class Library implements \Countable
{
private array $books = [];
public function add(Book $book): void
{
$this->books[] = $book;
}
public function count(): int
{
return count($this->books);
}
public function nextBook(): ?Book
{
return empty($this->books) ? null : $this->books[0];
}
}
Da forma que a classe Library
foi criada, o método nextBook
retorna apenas o primeiro livro existente na biblioteca, quando houver. Não precisamos nos preocupar com isso agora porque não temos nenhum teste validando o processo de iteração nos livros, precisamos apenas que os testes passem:
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.068, Memory: 4.00 MB
OK (1 test, 3 assertions)
01.03. Refatorar o código escrito (azul)
Agora sim fazemos melhorias naquele código que escrevemos anteriormente. Como ainda é o começo do nosso sistema e não tem muita coisa para mexer, talvez nem fosse preciso refatorar nada, mas vamos colocar um operador ternário no método nextBook
para diminuir um pouco o código:
public function nextBook(): ?Book
{
return $this->books[0] ?? null;
}
E rodamos o teste novamente para ter certeza de que nada quebrou:
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.067, Memory: 4.00 MB
OK (1 test, 3 assertions)
Agora precisamos fazer com que a segunda parte dos nossos requisitos seja atendida: o método nextBook
deve navegar por todos os livros disponíveis na biblioteca.
02.01. Criar um teste que falhe (vermelho)
Vamos criar um roteiro de como o sistema deveria funcionar, criando os livros, adicionando eles à biblioteca e passando por todos os ítens disponíveis:
public function testNavegarPorTodosOsLivros(): void
{
$book1 = new Book('O Mundo de Sofia', 'Jostein Gaarder');
$book2 = new Book('O Silmarillion', 'J. R. R. Tolkien');
$library = new Library();
$library->add($book1);
$library->add($book2);
self::assertEquals($book1, $library->nextBook());
self::assertEquals($book2, $library->nextBook());
}
Ao executar esse teste no PHPUnit vemos que ele irá falhar:
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 00:00.087, Memory: 4.00 MB
There was 1 failure:
1) Tests\BooksTest::testNavegarPorTodosOsLivros
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
App\Book Object (
- 'name' => 'O Silmarillion'
- 'author' => 'J. R. R. Tolkien'
+ 'name' => 'O Mundo de Sofia'
+ 'author' => 'Jostein Gaarder'
)
/app/tests/BooksTest.php:35
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.
ERROR: 1
Aqui o PHPUnit falou que fez duas afirmações e apenas uma delas falhou (Assertions: 2, Failures: 1
). Isso foi porque o primeiro item sempre está retornando, então pesquisar apenas pelo primeiro item iria funcionar. O problema está quando vamos pesquisar a partir do segundo item.
02.02. Fazer os testes passarem (verde)
Vamos mexer na classe Library
para que o método nextBook
passe por todos os livros disponíveis:
<?php
namespace App;
class Library implements \Countable
{
private array $books = [];
private int $nextBookIdx = 0;
public function add(Book $book): void
{
$this->books[] = $book;
}
public function count(): int
{
return count($this->books);
}
public function nextBook(): ?Book
{
if(isset($this->books[$this->nextBookIdx])) {
$book = $this->books[$this->nextBookIdx];
$this->nextBookIdx++;
return $book;
}
return null;
}
}
Adicionamos à classe uma variável chamada $nextBookIdx
que represente qual o índice do próximo livro a ser retornado e usamos essa variável na função nextBook()
, retornando o livro correspondente caso ele exista.
Ao chamar o PHPUnit vemos que tanto o novo teste quanto o anterior estão passando:
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.068, Memory: 4.00 MB
OK (2 tests, 5 assertions)
02.03. Refatorar o código escrito (azul)
Como a variável $nextBookIdx
só é usada dentro do nextBook()
e $books
é um array, podemos usar as funções nativas do PHP current()
e next()
para resolver a navegação, removendo a necessidade da variável $nextBookIdx
:
<?php
namespace App;
class Library implements \Countable
{
private array $books = [];
public function add(Book $book): void
{
$this->books[] = $book;
}
public function count(): int
{
return count($this->books);
}
public function nextBook(): ?Book
{
$book = current($this->books);
if($book) {
next($this->books);
return $book;
}
return null;
}
}
Para quem ainda não está familiarizado com essas funções do PHP, a documentação explica muito bem o que elas fazem, mas de forma geral o que acontece é o seguinte:
- Cada array possui um ponteiro interno para seu elemento atual, que é inicializado para o primeiro elemento inserido no array. O método
current()
retorna o elemento atual oufalse
caso não exista nada para retornar. - A função
next()
avança o ponteiro, retornandofalse
caso não exista um próximo item.
Confirmamos que os testes permanecem sem erros:
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.066, Memory: 4.00 MB
OK (2 tests, 5 assertions)
Se fosse preciso adicionar novas funcionalidades ao sistema continuaríamos nesse mesmo fluxo: criar um teste de falha → escrever o mínimo de código para fazer teste passar → melhorar o código. Não tem muito segredo.
Por que usar?
Todo esse processo pode parecer tedioso mas essa prática nos traz muitos benefícios durante o desenvolvimento e manutenção do sistema. Para finalizar queria deixar aqui os benefícios que percebi enquanto usava o TDD:
Qualidade de código
"Se algo não é possível ser testado, então foi desenvolvido de forma ruim."
— Código Limpo, cap. 09
Se nós criamos um código com muito acoplamento ou de alta complexidade não vamos querer testá-lo porque vai dar muito trabalho ou vai ser totalmente inviável. Começar escrevendo o teste e apenas o mínimo de código necessário nos força a pensar em como fazer as coisas de maneira mais simples e desacoplada possível.
Entendimento sobre o problema
Para escrever um teste nós precisamos entender bem o problema que estamos resolvendo e qual resultado queremos chegar. Descrever a ideia através do teste é como criar um roteiro dizendo como a aplicação precisa funcionar. Dessa forma não criamos coisas nem de mais e nem de menos, já criamos um código voltado para a solução esperada.
Segurança e produtividade
Quando a gente altera um código existe a chance dessa alteração causar algum tipo de imprevisto. Quando temos uma aplicação com alta cobertura de testes automatizados essa chance é menor. E se não precisamos nos preocupar tanto em quebrar alguma coisa ou em fazer testes manuais, sentimos mais segurança naquilo que estamos fazendo e ganhamos tempo para outras atividades.
Existe também o benefício de quando uma pessoa nova entra no time ou quando voltamos a mexer em um código que não vemos há semanas, os testes vão lembrar de todas as excessões que podem acontecer, todas as situações que precisam funcionar de certa maneira. Todos os contratos de como a aplicação deve funcionar estão assegurados.
Documentação
Os testes do seu código podem ser a melhor documentação que você vai ter. Eles te dizem o que funciona, como funciona, quais dependências estão envolvidas, quais os possíveis fluxos de erros, etc. E através do TDD, seus testes sempre estarão tão ou mais atualizados quanto o desenvolvimento, não haverá nenhum atraso.
Bom, é isso. Como sempre, se ficou alguma dúvida pode deixar aqui nos comentários que eu respondo assim que possível!
Se quiserem baixar o código é só acessar o repositório no GitHub. Cada etapa do ciclo que fizemos aqui pode ser encontrada lá na lista de branches ou de commits.
Fico por aqui e até a próxima!
Fonte:
PHP Testing Jargon, Laracasts.
F.I.R.S.T, Tim Ottinger & Jeff Langr
Testes Unitários com TDD, Pablo Rodrigo Darde.
Livro: Código Limpo, Robert C. Martin
Posted on June 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.