Testes em Python - Parte 1: Introdução
Tags: python, testes, iniciante, womenintech
Published: 2020-05-19 01:22:52



Essa é a primeira parte da série de testes em Python. 

A primeira parte tem por objetivo fazer uma introdução a testes automatizados utilizando o framework nativo do Python unnitest. Aqui, veremos alguns conceitos primordiais para escrever os primeiros casos de teste.

### Pré-requisito

- Conhecimento básico de Python

### Roteiro

1. O que são testes
2. Teste unitário (de unidade)
3. Conceitos
4. Exemplos
5. Considerações adicionais
6. Conclusão

---

## 1. O que são testes

### 1.1 Motivação

Ao escrever nossos programas, precisamos validar se o resultado que ele produz é o **correto**. 

Se tratando dos primeiros programas que escrevemos, normalmente rodamos o nosso código e passamos diversos valores para verificar se o que ele retorna é o esperado. Quando isso não acontece, temos que fazer uma varredura pelo nosso código e tentar entender o que ocorreu. Pode ter sido exibido [algum erro ou exceção ao usuário](https://docs.python.org/3/tutorial/errors.html) ou o programa retornar o valor errado e não exibir erro nenhum (ocasionado por erro de lógica na maioria das vezes). Nos dois casos, podemos usar o [modo debug](https://docs.python.org/3/library/pdb.html) (que são facilitados por uma IDE¹) ou adicionar alguns *outputs* no terminal usando a função *print.* Com isso, podemos identificar o problema e corrigir nosso código para retornar o que é esperado. **Isso é um teste manual.**

> IDE¹ (Integrated Development Environment) do português ambiente integrado de desenvolvimento. É um ambiente que nos ajuda a desenvolver nossos códigos sendo possível não só utilizar como editor de texto, mas também facilitar execução, debbug e integração com outras ferramentas. Em Python, o [Pycharm](https://www.jetbrains.com/pt-br/pycharm/) é uma IDE muito utilizada e difundida.

Repare que isso se torna extremamente cansativo e está sujeito a falhas fazer validações dessa forma. Assim, não garantimos a qualidade do nosso programa de forma facilitada se mais funcionalidades forem adicionadas pois todas as vezes teríamos que executar nosso programa e verificar os cenários na mão.

### 1.2 Testes automatizados

Testes automatizados são cenários que escrevemos através de linha de código simulando os testes manuais que antes eram feitos. Poupando assim tempo e esforço nessas verificações.

Escrever testes automatizados vai muito além do que somente escrever cenários para validar que o código faça o que deveria fazer. O teste também nos ajuda a:

- Deixar o código mais limpo (ajuda na remoção de [code smell](https://www.quora.com/What-are-some-common-Python-code-smells-and-how-should-one-refactor-them))
- Garantir maior manutenção do código
- Servir como documentação: de forma visual conseguimos saber quais são os cenários esperados e os tratamentos em caso de erro olhando o arquivo de teste
- Evitar trabalho manual (um teste automatizado é muito melhor do que um teste manual com *print*)
- Evitar bug's
- Prover feedback para quem está desenvolvendo a aplicação: conseguimos saber se o programa está retornando o que é esperado mesmo alterando a lógica do programa principal.

## 2. Teste unitário

Um teste unitário é a maneira de você testar as pequenas unidades existentes em seu código. Também é chamado de teste de unidade.

Para nos ajudar com os testes, iremos utilizar o framework built-in (nativo) do Python chamado [unittest](https://docs.python.org/3/library/unittest.html).

**Exemplo 1:**

```python
from unittest import main, TestCase

def square(x):
    return x ** 2

class TestSquare(TestCase):
    def test_if_returns_square_of_2(self):
        result = square(2)
        expected = 4

        self.assertEqual(result, expected)

if __name__ == '__main__':
    main()
```

No exemplo acima, estamos testando uma pequena unidade do nosso código, estamos validando um cenário da função chamada **square**.

Repare que todo o contexto de teste está sendo utilizado dentro de uma [classe](https://docs.python.org/pt-br/3/tutorial/classes.html).

Mas, para entender melhor como funciona o exemplo acima, vamos primeiro entender alguns conceitos.

## 3. Conceitos

### 3.1 Preparação do teste

Nessa etapa é feita a preparação do ambiente para fazer o teste rodar. Essa preparação é chamada de **fixture** e consiste nos itens necessários para um ou mais testes serem executados. 

- Exemplo: para conseguir testar uma função que lê um determinado arquivo precisamos de um arquivo no nosso ambiente de teste para ser possível fazer a validação

Pode conter o uso dos métodos [setUp() e tearDown()](https://riptutorial.com/python/example/13280/test-setup-and-teardown-within-a-unittest-testcase). Isso são ações que são executadas antes e depois, respectivamente, da execução de cada um dos cenários de teste.

### 3.2 Caso de teste

É o conjunto de cenários que queremos testar. 
Em um caso de teste agrupamos todos os pequenos cenários que queremos validar de forma unitária que fazem parte do mesmo contexto.

Com o framework unittest, usamos uma classe base chamada **TestCase**.

### 3.3 Asserções

Asserções servem para validar que o cenário do seu código ocorreu como o esperado.

O **assert** é um comando built-in (nativo) do Python. E podemos usar da seguinte forma:

![Assert built-in Python](https://dev-to-uploads.s3.amazonaws.com/i/mm6gdfastl51j2t95xcm.png)


Repare que na primeira asserção é comparada se a soma de 1+1 é igual a 2. Como o resultado é verdadeiro, nada é exibido no console.

Já na asserção seguinte, há uma falha e um erro é levantado do tipo **AssertionError** (erro de asserção).

O framework **unittest** facilita nessas asserções que vamos precisar fazer. As asserções completas podem ser vistas em sua própria [documentação](https://docs.python.org/3/library/unittest.html#assert-methods). 

### 3.4 Test Runner

Permite rodar a execução dos testes. O test runner orquestra a execução dos testes e exibe para o usuário os resultados.

Além de utilizar o [test runner do unittest](https://docs.python.org/3/library/unittest.html#command-line-interface) outros podem ser utilizados como o [pytest](https://docs.pytest.org/en/latest/usage.html).

### 3.5 Coverage

[Coverage](https://coverage.readthedocs.io/en/coverage-5.1/) remete a cobertura de testes, ou seja, o quanto o seu código está sendo testado.

Executando o coverage, conseguimos saber quais trechos de código foram testados e quais não foram. 

**ATENÇÃO:** 100% de cobertura é diferente de ter todos os cenários testados! Além de testar o fluxo principal do programa também precisamos testar casos inesperados (veremos mais adiante na série "Testes em Python").

### 3.6 Mock

[Mock](https://docs.python.org/3/library/unittest.mock.html) é uma biblioteca utilizada em testes quando queremos simular um determinado comportamento. Mock em inglês é literalmente "imitar". Ele é bastante utilizado quando nosso código se comunica com elementos externos como por exemplo: conexão com o banco de dados e chamadas HTTP. 

Se não for utilizado o mock, no momento que executarmos nosso código chamadas reais irão acontecer para a rede ou banco de dados, por exemplo (veremos mais adiante na série "Testes em Python").

## 4. Exemplos

![GIF cat typing in computer](https://media.giphy.com/media/o0vwzuFwCGAFO/giphy.gif)

Cat writing: show me the code!

### 4.1 Vamos entender o funcionamento do Exemplo 1

```python
from unittest import main, TestCase

def square(x):
    return x ** 2

class TestSquare(TestCase):
    def test_if_returns_square_of_2(self):
        result = square(2)
        expected = 4

        self.assertEqual(result, expected)

if __name__ == '__main__':
    main()

```

Na primeira linha importamos os itens necessários do framework unittest. 

`main()` ⇒  chamamos o **Test Runner** da biblioteca para ao rodar nosso código Python (python meu_arquivo.py).

 `TestCase` ⇒ fornece a estrutura necessária para montar o caso de teste.

```python
from unittest import main, TestCase
```

O próximo bloco se refere ao nosso código com o trecho que queremos validar

```python
def square(x):
    return x ** 2
```

Criamos uma classe de teste herdando o `TestCase` do unittest e seus métodos são os **casos de teste**.

Repare que dentro da classe escrevemos de modo descritivo o cenário que está sendo testado (estamos testando se o nosso código retorna o quadrado de dois). Depois fazemos uma **asserção** usando `assertEqual` comparando se a chamada da função **square** retorna o esperado, que é 4. 

```python
class TestSquare(TestCase):
    def test_if_returns_square_of_2(self):
        result = square(2)
        expected = 4

        self.assertEqual(result, expected)
```

**Nota:** Repare que o nome da classe e o cenário começam com a palavra "test". O nome "test" é obrigatório ser iniciado para as funções de teste, mas opcionais para a classe (no entanto é recomendado por questões de clareza).

Por fim, o último trecho chama o **main()** do unittest quando o arquivo python for executado.

```python
if __name__ == '__main__':
    main()
```

Para rodar o exemplo basta rodar como um arquivo python normalmente (no meu caso eu salvei em um arquivo my_first_test.py)

```bash
python my_first_test.py
```

No console será exibido que 1 teste foi executado e ele está com o status OK, ou seja, o teste passou.

![First test passed with success](https://dev-to-uploads.s3.amazonaws.com/i/wmnndym3cif6v5laub3b.png)


Vamos adicionar outro cenário de teste para simular um erro

```python
from unittest import main, TestCase

def square(x):
    return x ** 2

class TestSquare(TestCase):
    def test_if_returns_square_of_2(self):
        result = square(2)
        expected = 4

        self.assertEqual(result, expected)

    def test_if_returns_square_of_4(self):
        result = square(4)
        expected = 4

        self.assertEqual(result, expected)

if __name__ == '__main__':
    main()
```

Executando novamente o código acima temos o seguinte retorno:

![Simulating error: test did not pass](https://dev-to-uploads.s3.amazonaws.com/i/asekm37dde9oli4yz1hi.png)


Repare que adicionando o cenário **test_if_returns_square_of_4** executamos no total 2 testes e justamente esse cenário falhou. Isso por que chamando a função **square** ela retorna 16 e estamos tentando validar que o esperado a ser retornado é 4. 

Corrigindo o nosso código, ele deveria ficar da seguinte forma:

```python
from unittest import main, TestCase

def square(x):
    return x ** 2

class TestSquare(TestCase):
    def test_if_returns_square_of_2(self):
        result = square(2)
        expected = 4

        self.assertEqual(result, expected)

    def test_if_returns_square_of_4(self):
        result = square(4)
        expected = 16

        self.assertEqual(result, expected)

if __name__ == '__main__':
    main()
```

**Observação:** Os testes nos ajudam a identificar problemas no nosso código principal, mas o próprio cenário de teste também está suscetível a erros. Devemos sempre validar se o teste está bem escrito e se realmente testando o cenário esperado. 

**Observação 2:** Note que existe outra forma de calcular o quadrado de um número. No exemplo acima calculamos como `x*x` mas perceba que se mudarmos a forma que é calculada para `x ** 2` o resultado retornado é o mesmo e isso não afeta os nossos testes. Se executarmos novamente veremos que os cenários continuarão passando.

Um teste não deve enviesar a forma como é implementado o código. Por isso se efetuarmos refatorações² no código, rodando os testes conseguimos garantir se o comportamento é o mesmo retornado pelo código anterior.

> Refatoração² é o efeito de você mudar a implementação do código sem afetar o seu retorno (comportamento externo). A implementação é mudada de forma que o código fique mais organizado, mais limpo e melhor estruturado.

**Nota:** É uma boa prática separar o código principal do código dos testes, veremos isso no próximo exemplo.

### 4.2 Detecção de divisão

O exemplo abaixo mostra uma função que detecta uma divisão. Se a divisão for possível deverá retornar **verdadeiro**, se não deverá retornar **falso**.

Essa função está armazenada dentro de um arquivo chamado **division_detect.py**

```python
def division_detect(numerator: int, denominator: int) -> bool:
    if numerator / denominator:
        return True
    return False
```

*Código 4.2.1*

Vamos escrever um cenário de testes para essa função. Para isso, vamos criar um arquivo chamado **test_division_detect.py**

**Nota:** Apesar dos testes rodarem independente do nome do arquivo, é uma boa prática escrever o arquivo de teste começando com a palavra **"test"** assim como nossa classe.

O arquivo de teste possui o seguinte esqueleto:

```python
from unittest import TestCase

class TestDivisionDetect(TestCase):

    def test_it_returns_true_if_division_by_number_is_successful(self):
        pass
```

*Código 4.2.2*

Para escrever esse cenário de teste, queremos validar se a nossa função `division_detect` retorna `true` se a divisão for bem sucedida. 

A questão é: qual número vamos usar para fazer esse teste? Poderíamos escolher dois números aleatórios como 10 para o numerador e 2 para o denominador. Importando a função e comparando se o resultado retornado é verdadeiro com a asserção `assertTrue` temos o seguinte cenário de teste:

```python
from unittest import TestCase

from division_detect import division_detect

class TestDivisionDetect(TestCase):
    def test_it_returns_true_if_division_by_number_is_successful(self):
        result = division_detect(
            numerator=10, denominator=2
        )
        self.assertTrue(result)
```

*Código 4.2.3*

No teste acima, vamos validar se a divisão de  `10/2` é considerada válida. 

Vamos rodar o arquivo de teste com o comando abaixo:

```bash
python -m unittest test_division_detect.py
```

Como sabemos, o resultado da operação é 5 e `bool(5)` é verdadeiro

![Result of bool(5) is true](https://dev-to-uploads.s3.amazonaws.com/i/cqo7xfnrsvcdo0rczfcl.png)


Veremos que um teste rodou e o resultado foi **OK** (o cenário de teste passou)

![Test passed](https://dev-to-uploads.s3.amazonaws.com/i/4flju09uc6w112key7zv.png)


Mas, o que aconteceria se a gente mudasse o código principal no arquivo **division_detect.py** para esse código:

```python
def division_detect(numerator: int, denominator: int) -> bool:
    if numerator == 10:
        return True
    return False
```

*Código 4.2.4*

Claro que aqui estamos forçando um pouco a barra e alterando completamente a lógica do código principal, mas se os testes forem executados novamente o teste vai passar. 

Nesse caso o teste passou mas isso não é garantia que o cenário está sendo validado de fato. Para garantir que a divisão está sendo feita  vamos gerar números aleatórios tanto para o numerador quanto para o denominador. Para isso vamos usar a função do python `randint`

```python
from random import randint
from unittest import TestCase

from division_detect import division_detect

class TestDivisionDetect(TestCase):
    def test_it_returns_true_if_division_by_number_is_successful(self):
        result = division_detect(
            numerator=randint(0, 100000), denominator=randint(0, 100000)
        )
        self.assertTrue(result)
```

*Código 4.2.5*

Repare que tanto para o numerador como para o denominador está sendo gerado números aleatórios entre 0 e 100000 e se executarmos novamente o arquivo **division_detect.py** que foi escrito no **Código 4.2.4** vamos ver que o código não vai passar. Vamos voltar o código para que fique o mesmo do **Código 4.2.1**.

Olhando novamente para o cenário de teste, o que aconteceria se o denominador sorteado fosse 0?

![GIF infinity loop made by snake](https://media.giphy.com/media/3oFzmdjqH15YebLQ52/giphy.gif)

É, aqui temos um problema. A divisão de qualquer número por zero é **indefinida**!* 

> *[Divisão por zero é uma operação que tende ao infinito e portanto é dada como indefinida.](https://pt.khanacademy.org/math/algebra/introduction-to-algebra/division-by-zero/v/why-dividing-by-zero-is-undefined) 

Vamos adicionar um novo cenário de teste para validar isso, dessa vez vamos forçar que o denominador seja zero.

```python
from random import randint
from unittest import TestCase

from division_detect import division_detect

class TestDivisionDetect(TestCase):
    def test_it_returns_true_if_division_by_number_is_successful(self):
        result = division_detect(
            numerator=randint(0, 100000), denominator=randint(0, 100000)
        )
        self.assertTrue(result)

    def test_it_returns_false_if_division_by_number_is_not_possible(self):
        result = division_detect(numerator=randint(0, 100000), denominator=0)
        self.assertFalse(result)
```

*Código 4.2.6*

Rodando novamente esse arquivo de testes, vamos receber o seguinte retorno

![Zero division error: test failed](https://dev-to-uploads.s3.amazonaws.com/i/nfn9yzlxgnig0nohesfm.png)

Repare que dois testes foram executados, o que é correto pois escrevemos dois cenários de teste. Mas um dos testes, o `test_it_returns_false_if_division_by_number_is_not_possible`, falhou. Isso pois aconteceu um erro chamado `ZeroDivisionError` (https://docs.python.org/3/library/exceptions.html#ZeroDivisionError). 

Olhando nosso código principal, vemos que não está sendo feito nenhum tipo de tratamento para esse erro. Vamos refatorar então o nosso código para lidar com esse erro, para isso vamos usar o [statement try/except do Python](https://docs.python.org/3/tutorial/errors.html#handling-exceptions). 

```python
def division_detect(numerator: int, denominator: int) -> bool:
    try:
        numerator / denominator
    except ZeroDivisionError:
        return False
    else:
        return True
```

*Código 4.2.7*

No código acima primeiro tentamos fazer a divisão entre os dois parâmetros recebidos, se a operação for bem sucedida irá retornar `True`, mas se acontecer uma exceção do tipo `ZeroDivisionError` irá retornar `Falso`. 

Rodando novamente os testes com `python -m unittest test_division_detect.py` vamos ver que os dois cenários passaram.

![Two tests passed with success](https://dev-to-uploads.s3.amazonaws.com/i/cjvcifhn4fjbnr9bjq2b.png)

**Nota:** Repare que nosso código original não estava preparado para uma divisão por zero. Isso é chamado de corner case, ou seja, são cenários que podem acontecer fora do padrão esperado. Para tratar esse cenário tivemos que adicionar um tratamento com as exceções do Python.

No arquivo de teste, para os dois cenários que estamos testando, estamos repetindo `randint(0, 100000)` . Como queremos pegar sempre um numerador aleatório para todos os testes, podemos utilizar o `setUp()`(https://docs.python.org/3/library/unittest.html#unittest.TestCase.setUp) que é uma inicialização que é executada sempre no início de cada cenário de teste. 

Por fim, chegaremos ao código abaixo:

```python
from random import randint
from unittest import TestCase

from division_detect import division_detect

class TestDivisionDetect(TestCase):
    def setUp(self) -> None:
        self.random_numerator = randint(0, 100000)

    def test_it_returns_true_if_division_by_number_is_successful(self):
        result = division_detect(
            numerator=self.random_numerator, denominator=randint(1, 100000)
        )
        self.assertTrue(result)

    def test_it_returns_false_if_division_by_number_is_not_possible(self):
        result = division_detect(numerator=self.random_numerator, denominator=0)
        self.assertFalse(result)
```

Repare que o denominador no primeiro cenário sorteia um número entre 1 e 100000, pois estamos querendo validar um cenário onde a divisão é bem sucedida.

## 5. Considerações adicionais

Aqui, listo alguns pontos que devem ser levados em consideração no momento de programar e testar seus códigos:

- Divida para conquistar: Mantenha a estrutura de arquivos organizadas. Se um projeto é grande, é sempre uma boa prática dividir em arquivos menores (isso facilita a manutenção e legibilidade)
- Um teste também deve ser limpo igual ao código principal (Clean Code - Robert C. Martin)
- Use nomes descritivos para as funções de teste, mesmo que seja um nome muito longo
- Pense em corner cases (cenários fora do padrão esperado), igual fizemos na divisão por zero no exemplo 2
- Um teste não deve engessar a implementação do seu código, igual vimos no exemplo 1 (o quadrado de um número pode ser feito de duas formas)
- Escrever em pequenas unidades ajudam a testar o código e a melhorar sua clareza
- Refatore: Sempre que puder melhorar seu código melhore! (Lema de escoteiro)

De acordo com a [PEP20](https://www.python.org/dev/peps/pep-0020/#id3)³:

> Errors should never pass silently

PEP³ (do inglês (Python Enhancement Proposals) se refere a propostas de como utilizar o Python da melhor forma: https://www.python.org/dev/peps/

## 6. Conclusão

Testes são uma maneira de garantir que o seu programa **retorna o** **resultado esperado.** Além disso, garante maior qualidade no produto que está sendo entregue. Os testes também ajudam que a equipe de desenvolvedores entendam os cenários que acontecem na aplicação e ajudam a identificar cenários fora do padrão. Testando nosso código, conseguimos encontrar também maneiras de deixar nosso código mais limpo e conciso, facilitando assim na manutenção futura e a evitar bugs que possam ocorrer. É melhor que um teste pegue o erro do que o cliente usando o seu produto :) 

Essa foi a primeira série de Testes em Python, espero que tenha ficado claro o entendimento dessa introdução e em breve teremos mais publicações sobre esse assunto. Dúvidas podem ser colocadas no comentário e lembre-se que testar faz parte do processo de escrever um bom código, segundo o programador Pete Goodliffe

> "Um bom código não surge do nada. [...] Para ter um bom código é preciso trabalhar nele. Arduamente. E você só terá um código bom se realmente se importar com códigos bons."

Como Ser Um Programador Melhor: um Manual Para Programadores que se Importam com Código (Pete Goodliffe)

![Gif that's all folks](https://media.giphy.com/media/lD76yTC5zxZPG/giphy.gif)