[Python] Exceções - Transformando bugs em features
Tags: python, pytest
Published: 2021-02-22 03:09:37


Neste post veremos como transformar os bugs em features, em outras palavras, como lidar com "erros" ao desenvolvermos nossas regras.

Sendo assim, aqui falaremos sobre como lançar exceções com o `rise`, como tratá-las utilizando o `try/except` e como criar exceções personalizadas para melhorar a legibilidade do nosso código.

Estaremos utilizando a linguagem Python, mas os conceitos abordados servirão para qualquer linguagem de programação que siga estes princípios de tratamento e lançamento de exceções.

###  Índice
- [Erros e Exceções](#erros-excecoes)
- [Bug vs Exceções](#bug-exception)
- [Tratando erros com o try/except](#try-except)
- [Lançando exceções](#rise)
- [Exceções personalizadas](#excecoes-personalizadas)
- [Conclusão](#conclusao)
- [Bônus - Pytest](#bonus)


#  <a id="erros-excecoes" />Erros e Exceções

### Por que lançar e tratar exceções?
É muito comum encontrar códigos como este:
```
def diga_ola(nome):
    if len(nome) < 5:
        return false
    elif len(nome) > 100:
        return false
    elif name.isnumeric():
        return false
    else:
        return f'ola {nome}'
```

Bom, claramente essas verificações estará relacionada com nossa regra de negócio. Sendo assim, para entender seu funcionamento teremos que ler e interpretar o que está acontecendo e em qual ordem.

O problema é quando executamos a função e ela retorna um `false` na nossa cara. Mas e aí?! Onde falhou?? Em qual parte da regra ela não passou?? E é nessas horas que fazemos o velho `print('aqui 1')`, `print('aqui 2')`, `print('aqui N')` para saber até onde o código está chegando. 

Na tentativa de "melhorar" ainda fazemos assim:
```
'''
Função para dizer olá
'''
def diga_ola(nome):
   // Verifica se o nome é menor que 5
    if len(nome) < 5:
        return false
    // Verifica se o nome é maior que 100
    elif len(nome) > 100:
        return false
    // Verifica se o nome é um número
    elif name.isnumeric():
        return false
    // Retornao nome formatado: Olá + nome
    else:
        return f'ola {nome}'
```

Se antes já estava ruim, agora que piorou. Nada *pythonico*!
Vamos ver formas melhores de organizar este código.

#### <a id="bug-exception" />Bug vs Exceções
Resumidamente
- Exceções são erros esperados, previsíveis e conhecidos de alguma situação. Ex: Divisão entre dois números `x / y`. Já é esperado que ocorra um erro caso o valor `y == 0`, logo podemos criar formas de tratar antes que trave o sistema.
- Já o bug é um erro desconhecido, algo que não estava sendo esperado em uma funcionalidade. Ex: Exibir um alerta no navegador do usuário. Poderá acontecer N situações, como tamanhos de telas diferentes, navegadores diferentes, erro de conexão com a internet, desabilitar a execução de scripts no navegador, etc. Apesar de imarginarmos e tentar tratar algum deles, em algum momento essa "exceção não prevista" poderá travar o funcionamento de uma página.

**Um bug conhecido poderá ser uma exceção tratada!**

Caso queira se aprofundar nas terminologias poderá pesquisar sobre o padrão *IEEE nº 610.12-1990* ou neste artigo da Dev Media [Gestão de defeitos](https://www.devmedia.com.br/gestao-de-defeitos-ferramentas-open-source-e-melhores-praticas-na-gestao-de-defeitos/8036)

### <a id="try-except" />Tratando erros com o try/except

Tente executar o cód a baixo
```
print(1/0)
```

O resultado será uma mensagem de erro:
```
Traceback (most recent call last):
	...
ZeroDivisionError: division by zero
```

Podemos melhorar esta mensagem tratando a exceção e isolando-a numa função
```
def  divide_numeros(dividendo, divisor):
    try:
        print(dividendo/divisor)
    except:
        print ('O divisor não pode ser 0.')
  
divide_numeros(1, 0)
```

O resultado será: `O divisor não pode ser 0.`

Mas o que acontece se executarmos: `divide_numeros(1, 'teste')` ?
O resultado será exatamente a mesma mensagem! Portanto, precisamos tratar as exceções individualmente e exibir a mensagem adequada.
Podemos reescrever assim:
```
def  divide_numeros(dividendo, divisor):
    try:
        print(dividendo/divisor)
    except  ZeroDivisionError:
        print ('O divisor não pode ser 0.')
    except  TypeError:
        print('Insira apenas números')
```
# <a id="rise" />Lançando exceções
Vamos falar agora de exceções relacionados a nossa regra de negócio.
Vamos rever o cenário onde nosso sistema terá uma mensagem de saudação para um usuário. Porém teremos algumas regras:
- O nome não poderá ser números;
- O nome deverá ter menos que 100 caracteres
- O nome deverá ter mais que 5 caracteres

Como já vimos antes, encher o código de *if* e *else* não é legal. Por isso faremos um código com tratamento de exceções. Começaremos criando nossa função:
```
def  diga_ola(name: str) -> str:
    return f'Olá {nome}'
```
O método acima recebe uma string por parâmetro e retorna uma string como resposta. Então poderíamos executar da seguinte maneira: `diga_ola('Flávio')`.

Como estamos especificando que o parâmetro recebido deverá ser uma string, ao tentar passar um número teremos um erro: `diga_ola(123)`
```
AttributeError: 'int' object has no attribute 'isnumeric'
```

Sendo assim, já podemos capturar nossa primeira exceção
```
try:
    print(diga_ola(123))
except  AttributeError:
    print('Insira apenas texto')
```

Para deixarmos dinâmico, vamos pedir através de um input para que o nome seja inserido:
```
try:
    name = input('Insira seu nome: ')
    print(diga_ola(name))
except  AttributeError:
    print('Insira apenas texto')
```

Faça o seguinte teste:
- Execute seu código
- Insira um o número `123`
- O resultado será: `Olá 123`

Como assim? Acabamos de tratar este erro...
O problema é que quando se trata de um **input** todo dado é uma string, logo atende ao nosso requesido. Por isso, precisaremos tratar dentro de nosso método e "lançar" uma nova exceção.

##  Lançando uma exceção
Para *lançar* uma nova exceção utilizaremos a palavra reservada `rise` e dizer qual é seu tipo e a mensagem. Vamos modificar nosso método:
```
def  diga_ola(name: str) -> str:
    if name.isnumeric():
        raise  TypeError('Apenas letras')
    return f'Olá {nome}'
```
No trecho `raise  TypeError('Apenas letras')` estamos lançando um exceção do tipo `TypeError` e passando por parâmetro a mensagem que será exibido.

Conheça a lista de exceções padrões que poderá ser lançada que são nativas da linguagem Python na [docs](https://docs.python.org/3/library/exceptions.html).

Já seguindo este curso, vamos implementar as outras regras:
```
def  diga_ola(name: str) -> str:
    if  name.isnumeric():
        raise  TypeError('Apenas letras.')  

    if  len(name) > 100:
        raise  OverflowError('Nome muito grante.')

    if  len(name) < 5:
        raise  ValueError('Nome pequeno.')

    return  f'Olá {name}'
```

E podemos chamá-lo assim:
```
try:
    name = input('Insira seu nome: ')
    print(diga_ola(name))
except  AttributeError:
    print('Insira apenas texto')
except  ValueError  as  error:
    print(error)
```

Está bem melhor de entender, mas ainda podemos melhorar.
Ao verificar se o parâmetro é menor que 5, nós lançamos uma função genérica e exibindo sua mensagem. Será que poderíamos criar "Exceções Personalizadas" e tratá-las da maneira ideal e não de forma genérica?

##  <a id="excecoes-personalizadas" />Exceções personalizadas
Criar exceções personalizadas nos ajudará a deixar nosso código mais organizado e legível.
Vamos criar  um arquivo chamado `exceptions.py` com o seguinte conteúdo:
```
class  NomePequenoException(ValueError):
    pass
```
Aqui nós estamos criando uma classe que estende a classe `ValueError`. Apesar de não executar nada, nos será muito útil na leitura do código. Veja nosso código final completo, onde faremos a importação da exceção e tratamos de forma individual:

```
import  Exceptions
def  diga_ola(name: str) -> str:
    if  name.isnumeric():
        raise  ValueError('Apenas letras.')  

    if  len(name) > 100:
        raise  OverflowError('Nome muito grante.')

    if  len(name) < 5:
        raise  Exceptions.NomePequenoException('Nome pequeno.')

    return  f'Olá {name}'

def  main():
    try:
        name = input('Insira seu nome: ')
        print(diga_ola(name))
    except  AttributeError:
        print('Insira apenas texto')
    except  Exceptions.NomePequenoException:
        print('Insira nomes com 5 ou mais letras')
    except  ValueError  as  error:
        print(error)

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

# <a id="conclusao" /> Conclusão
Desenvolver um bom código não é só "o que funciona", mas também é se preocupar com a organização e legibilidade para facilitar a manutenção. Boa parte do tempo de desenvolvimento é encontrando falhas. Por isso, organizar as exceções ao desenvolver novas features nos ajudará a rastrear o erro e facilitará no [processo de teste](#testes).

No exemplo que criamos  temos o caso onde a função será chamada em apenas um lugar. Mas em um sistema maior, onde será chamado em vários lugares, por várias pessoas, poderemos ter formas diferentes de tratar estas exceções: fazendo um print, registrando um log, retornando a mensagem para um dashboard em um frontend, etc. Por isso, é uma boa prática deixar a cargo de quem executa o método decidir o que fazer quando houver a exceção.

> Dica: Ficamos presos em criar o menor número de linhas, a menor quantidade de arquivos, os nomes mais compactos de variáveis e métodos. Mas hoje, com a grande capacidade computacional e diversas ferramentas para compactar nosso código, a quantidades de bytes em um arquivo é o menor dos problemas. Sendo assim, não exite em criar arquivos de exceções, testes e escrever nomes coerentes para seus componentes.



# <a id="bonus" />  Bônus: pytest
Deixo aqui minha sugestão *off topic* para o estudo de testes automatizados. A baixo terá as instruções para executar os testes aplicados a nosso exemplo.

##  Criando ambiente

###  [venv](https://docs.python.org/3/library/venv.html)
`python3 -m venv venv`

Caso esteja no Windows e tenha erro ao encontrar o Python, tente executar assim ou consulte a [documentação](https://docs.python.org/3/library/venv.html):
`py -m venv venv`

###  Ative o ambiente
Linux: `source venv/bin/activate`
Windows: `venv/scripts/activate`

###  Instalando o pytest
`pip install pytest`

###  Arquivo te testes
Crie o arquivo `test_exceptions.py` (todos os testes deverão começar com **test_***)

```
import  pytest
import  random
import  Exceptions
from  main  import  diga_ola


def  test_quando_o_nome_for_composto_maior_que_5_letras():
    assert  diga_ola('Flávio Filipe') == 'Olá Flávio Filipe'

  
def test_quando_o_nome_for_menor_que_5_letras_nao_deve_permitir():
    with  pytest.raises(Exceptions.NomePequenoException):
        diga_ola('abc')


def teste_quando_inserir_numero_nao_deve_permitir():
    with  pytest.raises(TypeError):
        diga_ola('123')
def  test_nome_maior_que_100_letras():
    with  pytest.raises(OverflowError):
        nome = 'a'*100+'a'

diga_ola(nome)
```

> Os trechos com `with pytest.raises(...):` irá verificar se o retorno do contexto analisado será o erro especificado por parâmetro.
> 
###  Executando os testes
Execute na raiz do projeto:
`pytest`