DEV Community

Leandro Duarte
Leandro Duarte

Posted on

🐍 Práticas recomendadas para testes de unidade em Python 🧪

Introdução

Os testes de unidade são uma parte essencial do processo de desenvolvimento de software, permitindo que os desenvolvedores verifiquem se suas unidades de código (funções, classes, métodos) estão funcionando corretamente. Em Python, existem várias práticas recomendadas que podem melhorar a eficácia e a qualidade dos testes de unidade. Aqui estão exemplificada as 5 primeiras. O restante irei apenas citar, mas é importante que você possa procurar sobre o assunto em outras fontes.

1️⃣ - Estrutura de Testes

Python oferece diversas bibliotecas populares para testes de unidade, como o unittest, pytest e nose. Essas estruturas fornecem uma série de recursos úteis, como assertivas, fixtures e relatórios de testes.

A seguir um exemplo com pytest

# test_calculadora.py
import pytest

def soma(a, b):
    return a + b

def test_soma():
    assert soma(2, 3) == 5
    assert soma(0, 0) == 0
    assert soma(-1, 1) == 0

def test_soma_negativos():
    assert soma(-5, -7) == -12
    assert soma(-10, 10) == 0
    assert soma(-2, 2) == 0
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos um módulo chamado test_calculadora.py que contém duas funções de teste: test_soma() e test_soma_negativos(). Essas funções são identificadas pelo prefixo "test_" e serão reconhecidas e executadas pelo pytest.

Cada função de teste contém uma ou mais assertivas, que verificam se os resultados da função soma() estão corretos. Se todas as assertivas passarem, o teste é considerado bem-sucedido. Caso contrário, uma falha será relatada, indicando que algo está errado no código.

Para executar esses testes, você precisa ter o pytest instalado em seu ambiente. Em seguida, você pode executar o pytest no diretório que contém o arquivo de teste. O pytest encontrará automaticamente todas as funções de teste e as executará:

$ pytest
Enter fullscreen mode Exit fullscreen mode

O pytest fornecerá um relatório de execução dos testes, indicando quais testes passaram e quais falharam.

Essa é apenas uma pequena amostra do uso da biblioteca pytest para testes de unidade em Python. Ela oferece muitos recursos adicionais, como fixtures (para configurações pré e pós-teste), parâmetros de teste parametrizados, captura de exceções esperadas e muito mais. Com o pytest, você pode escrever testes concisos, legíveis e eficazes para garantir a qualidade do seu código.

2️⃣ - Testes Independentes

Cada teste deve ser independente e não depender do resultado de outros testes. Isso ajuda a isolar problemas e torna os testes mais confiáveis.

Aqui está um exemplo que demonstra a escrita de testes independentes usando a biblioteca unittest em Python:

import unittest

def soma(a, b):
    return a + b

class TestCalculadora(unittest.TestCase):
    def test_soma_positivos(self):
        resultado = soma(2, 3)
        self.assertEqual(resultado, 5)

    def test_soma_negativos(self):
        resultado = soma(-5, -7)
        self.assertEqual(resultado, -12)

    def test_soma_zero(self):
        resultado = soma(0, 0)
        self.assertEqual(resultado, 0)
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos uma classe de teste chamada TestCalculadora, que herda da classe unittest.TestCase. Dentro dessa classe, temos três métodos de teste: test_soma_positivos(), test_soma_negativos() e test_soma_zero().

Cada método de teste é independente e não depende dos resultados de outros testes. Cada um deles chama a função soma() com diferentes argumentos e verifica se o resultado é o esperado usando a assertiva self.assertEqual().

Para executar esses testes usando a estrutura de testes unittest, você pode criar um arquivo separado com o seguinte código:

import unittest

# Importe a classe de teste
from test_calculadora import TestCalculadora

# Carregue os testes da classe
suite = unittest.TestLoader().loadTestsFromTestCase(TestCalculadora)

# Crie um TestRunner
runner = unittest.TextTestRunner()

# Execute os testes
result = runner.run(suite)
Enter fullscreen mode Exit fullscreen mode

Ao executar esse arquivo, você verá o resultado dos testes, indicando se cada um passou ou falhou. Cada teste é executado de forma independente, garantindo que problemas em um teste não afetem os resultados dos outros testes.

Ao escrever testes independentes, você pode ter confiança de que cada teste está sendo executado em um contexto isolado, o que torna os testes mais confiáveis e facilita a identificação e correção de problemas específicos.

3️⃣ - Nome descritivos aos testes

Escolha nomes significativos para seus testes, descrevendo o comportamento que eles estão testando. Isso facilita a identificação de problemas quando os testes falham e ajuda a documentar a intenção do teste.

Aqui está um exemplo que ilustra a escolha de nomes significativos para os testes, descrevendo o comportamento que eles estão testando:

import unittest

def adicionar(a, b):
    return a + b

class TestCalculadora(unittest.TestCase):
    def test_adicionar_numeros_positivos(self):
        resultado = adicionar(2, 3)
        self.assertEqual(resultado, 5)

    def test_adicionar_numeros_negativos(self):
        resultado = adicionar(-5, -7)
        self.assertEqual(resultado, -12)

    def test_adicionar_numero_positivo_e_negativo(self):
        resultado = adicionar(10, -5)
        self.assertEqual(resultado, 5)
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos uma classe de teste chamada TestCalculadora que contém três métodos de teste: test_adicionar_numeros_positivos(), test_adicionar_numeros_negativos() e test_adicionar_numero_positivo_e_negativo().

Cada método de teste possui um nome descritivo que indica claramente o comportamento sendo testado. Por exemplo, o método test_adicionar_numeros_positivos() verifica se a função adicionar() retorna o resultado correto ao adicionar dois números positivos. Da mesma forma, os outros métodos de teste descrevem o comportamento específico sendo testado.

Ao escolher nomes significativos para os testes, fica mais fácil identificar qual aspecto do código está sendo testado quando ocorre uma falha. Além disso, esses nomes ajudam a documentar a intenção do teste, permitindo que outros desenvolvedores compreendam rapidamente o que está sendo testado e qual é o resultado esperado.

Isso torna os testes mais legíveis, facilita a manutenção e melhora a clareza da documentação associada aos testes.

4️⃣ - Use mocks e stubs

Quando um código depende de recursos externos, como bancos de dados ou APIs, é recomendado o uso de mocks ou stubs para simular essas dependências durante os testes. Isso torna os testes mais rápidos e independentes de fatores externos.

Aqui está um exemplo que demonstra o uso de mocks para simular uma dependência externa durante os testes:

import unittest
from unittest.mock import MagicMock

class API:
    def get_data(self):
        # Lógica para obter dados de uma API externa
        pass

def processar_dados_da_api(api):
    dados = api.get_data()
    # Lógica para processar os dados recebidos da API
    return dados

class TestProcessamento(unittest.TestCase):
    def test_processar_dados_da_api(self):
        # Criação do mock da API
        mock_api = MagicMock()
        mock_api.get_data.return_value = {'valor': 42}

        # Execução do código que depende da API
        resultado = processar_dados_da_api(mock_api)

        # Verificação do resultado
        self.assertEqual(resultado, {'valor': 42})
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos uma classe API que representa uma dependência externa, como uma API que retorna dados. Em seguida, temos uma função processar_dados_da_api() que depende dessa API para obter e processar os dados.

Durante o teste, criamos um mock da classe API usando MagicMock da biblioteca unittest.mock. Configuramos o retorno do método get_data() do mock para simular os dados que seriam obtidos da API real.

Ao chamar a função processar_dados_da_api() com o mock da API, estamos simulando o comportamento da dependência externa durante o teste, sem precisar realmente fazer uma chamada à API real. Isso torna o teste mais rápido, independente de recursos externos e não está sujeito a falhas ou variações externas.

Em seguida, podemos verificar se o resultado do processamento está correto usando as assertivas apropriadas, como self.assertEqual().

O uso de mocks ou stubs para simular dependências externas é uma prática recomendada ao testar código que depende de recursos externos, como bancos de dados, APIs, serviços web, entre outros. Isso permite que você isole o código que está sendo testado e torne os testes mais rápidos, previsíveis e independentes de fatores externos.

5️⃣ - Cubra diferentes casos de teste

Cubra diferentes casos de teste: Certifique-se de criar casos de teste que cubram todas as ramificações do código, incluindo casos de teste positivos e negativos. Isso ajuda a identificar erros e aumenta a confiabilidade do código testado.

Aqui está outro exemplo que demonstra a cobertura de diferentes casos de teste, incluindo casos positivos e negativos:

import unittest

def validar_idade(idade):
    if idade < 0:
        raise ValueError("A idade não pode ser negativa")
    elif idade < 18:
        return "Menor de idade"
    else:
        return "Maior de idade"

class TestValidacaoIdade(unittest.TestCase):
    def test_idade_positiva_menor(self):
        resultado = validar_idade(15)
        self.assertEqual(resultado, "Menor de idade")

    def test_idade_positiva_maior(self):
        resultado = validar_idade(25)
        self.assertEqual(resultado, "Maior de idade")

    def test_idade_zero(self):
        resultado = validar_idade(0)
        self.assertEqual(resultado, "Menor de idade")

    def test_idade_negativa(self):
        with self.assertRaises(ValueError):
            validar_idade(-10)
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos uma função validar_idade() que recebe uma idade como parâmetro e retorna uma string indicando se a idade é "Menor de idade" ou "Maior de idade". Ela inclui verificações para lidar com idades negativas.

A classe de teste TestValidacaoIdade contém quatro métodos de teste diferentes:

  • test_idade_positiva_menor() verifica se a função validar_idade() retorna corretamente "Menor de idade" para uma idade positiva abaixo de 18.

  • test_idade_positiva_maior() verifica se a função validar_idade() retorna corretamente "Maior de idade" para uma idade positiva igual ou acima de 18.

  • test_idade_zero() verifica se a função validar_idade() retorna corretamente "Menor de idade" para uma idade igual a zero.

  • test_idade_negativa() verifica se a função validar_idade() lança corretamente uma exceção ValueError ao receber uma idade negativa.

Esses casos de teste cobrem diferentes cenários relacionados à validação da idade. Eles incluem idades positivas menores e maiores de 18, além de abordar os casos de idade igual a zero e idades negativas.

Ao criar casos de teste que abrangem essas ramificações do código, podemos identificar possíveis erros e garantir que o código seja confiável em diferentes cenários. Isso aumenta a cobertura de testes e a confiabilidade do código testado.

6️⃣ - Automatize os testes:

Integre os testes de unidade em um pipeline de integração contínua (CI) para garantir que eles sejam executados regularmente. Isso ajuda a detectar problemas rapidamente e a evitar regressões no código.

7️⃣ - Aplique o princípio do "AAA":

A estrutura de teste deve seguir o padrão "Arrange, Act, Assert" (Preparar, Agir, Verificar). Na seção "Arrange", defina o ambiente de teste e configure os objetos necessários. Na seção "Act", invoque o código a ser testado. E na seção "Assert", verifique se o resultado está correto.

8️⃣ - Aproveite os recursos de assertivas:

Utilize as assertivas fornecidas pela estrutura de testes para verificar se os resultados são os esperados. Isso inclui assertivas de igualdade, exceções lançadas, presença de elementos em listas, entre outros.

9️⃣ - Execute testes de forma rápida e frequente:

É importante executar os testes de unidade com frequência, preferencialmente após cada alteração no código. Isso ajuda a identificar problemas o mais cedo possível e a manter a confiabilidade do código.

🔟 - Mantenha uma cobertura de testes adequada:

Acompanhe a cobertura dos testes para garantir que seu código esteja adequadamente testado. Existem ferramentas, como o coverage, que podem ajudar a medir a cobertura do código testado. Busque manter uma cobertura abrangente, cobrindo o máximo possível do código.

1️⃣1️⃣ - Refatore os testes conforme necessário:

À medida que o código evolui, os testes também devem ser atualizados. Se ocorrerem mudanças significativas no código, verifique se os testes ainda são relevantes e os atualize, se necessário.

1️⃣2️⃣ - Documente seus testes:

É útil fornecer uma documentação clara dos testes de unidade. Descreva o propósito de cada teste, os casos de teste cobertos e as expectativas de resultados. Isso facilita a compreensão e a manutenção dos testes no futuro.

1️⃣3️⃣ - Compartilhe seus testes:

Se você está trabalhando em um projeto de código aberto ou em equipe, compartilhar seus testes é uma prática recomendada. Isso permite que outros desenvolvedores vejam como usar sua funcionalidade e possam contribuir com melhorias ou correções.

1️⃣4️⃣ - Execute testes em diferentes ambientes:

Considere a execução de testes de unidade em diferentes ambientes, como diferentes sistemas operacionais ou versões do Python, para garantir a portabilidade e a compatibilidade do código.

1️⃣5️⃣ - Utilize ferramentas de análise estática:

Além dos testes de unidade, considere o uso de ferramentas de análise estática de código, como o pylint ou flake8, para identificar possíveis problemas ou más práticas no código.

Finalização

Lembre-se de que os testes de unidade são apenas uma parte do processo de garantia de qualidade de software. É importante complementá-los com outros tipos de testes, como testes de integração e testes de aceitação, para obter uma cobertura abrangente e confiável do código.

Ao seguir essas práticas recomendadas, você estará no caminho certo para escrever testes de unidade eficientes e de alta qualidade em Python. Eles ajudarão a melhorar a robustez do seu código, facilitarão a manutenção e aumentarão a confiança na funcionalidade do software desenvolvido.

Top comments (0)