Introdução ao desenvolvimento guiado por testes (TDD)
Tags: braziliandevs, testing, womenintech, tdd
Published: 2021-06-14 18:33:11

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!

![Ciclo do TDD - novo teste com erro (vermelho), o mínimo de código para passar o teste (verde), refatorar (azul)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ri6ms1o7n2aecgfmbume.png)

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:

```php
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
<?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
<?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:

```php
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:

```php
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
<?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
<?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 ou `false` caso não exista nada para retornar.
- A função `next()` avança o ponteiro, retornando `false` 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](https://github.com/thmarra/intro-tdd). 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:**

[Documentação do PHP](https://www.php.net)

[PHP Testing Jargon](https://laracasts.com/series/php-testing-jargon), Laracasts.

[F.I.R.S.T](http://agileinaflash.blogspot.com/2009/02/first.html), Tim Ottinger & Jeff Langr

[Testes Unitários com TDD](https://medium.com/@pablodarde/testes-unit%C3%A1rios-com-tdd-test-driven-development-657f3dadad06), Pablo Rodrigo Darde.

Livro: Código Limpo, Robert C. Martin