Skip to content

Python unittest: Escreva e Execute Testes Unitários (Guia Completo)

Updated on

Você escreve uma função que processa dados de usuários. Ela funciona quando você testa manualmente com algumas entradas. Então um colega altera uma função auxiliar da qual ela depende, e seu código passa a retornar resultados errados silenciosamente por três semanas antes de alguém notar. O bug chega à produção. Registros de clientes são corrompidos. A causa raiz foi uma mudança de uma única linha que ninguém validou contra o comportamento existente. Esse é exatamente o tipo de falha que o teste unitário evita. O Python vem com unittest na biblioteca padrão — um framework de testes completo que detecta regressões antes que cheguem à produção, sem dependências de terceiros.

📚

O que é unittest e por que usá-lo?

unittest é o framework de testes embutido do Python, modelado a partir do JUnit do Java. Ele faz parte da biblioteca padrão desde o Python 2.1, o que significa que toda instalação do Python já o inclui. Sem pip install. Sem gerenciamento de dependências. Você escreve classes de teste, define métodos de teste e executa tudo pela linha de comando.

Testes unitários verificam se partes individuais do código (funções, métodos, classes) se comportam corretamente em isolamento. Quando cada unidade funciona por conta própria, integrar tudo tende a produzir bem menos bugs ocultos.

Veja o que o unittest oferece “de fábrica”:

  • Classes de caso de teste com descoberta automática de testes
  • Métodos de asserção ricos (igualdade, verificação booleana, exceções, warnings)
  • Hooks de setUp/tearDown tanto no nível de método quanto de classe
  • Suites de teste para organizar e agrupar testes
  • Mocking via unittest.mock (adicionado no Python 3.3)
  • Runner de testes por linha de comando com controles de verbosidade

Seu primeiro teste unitário

Comece com uma função para testar e uma classe de teste correspondente.

# calculator.py
def add(a, b):
    return a + b
 
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# test_calculator.py
import unittest
from calculator import add, divide
 
class TestCalculator(unittest.TestCase):
 
    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)
 
    def test_add_negative_numbers(self):
        self.assertEqual(add(-1, -1), -2)
 
    def test_add_zero(self):
        self.assertEqual(add(0, 0), 0)
 
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5.0)
 
    def test_divide_by_zero_raises(self):
        with self.assertRaises(ValueError):
            divide(10, 0)
 
if __name__ == "__main__":
    unittest.main()

Execute:

python -m unittest test_calculator.py -v

Saída:

test_add_negative_numbers (test_calculator.TestCalculator) ... ok
test_add_positive_numbers (test_calculator.TestCalculator) ... ok
test_add_zero (test_calculator.TestCalculator) ... ok
test_divide_by_zero_raises (test_calculator.TestCalculator) ... ok
test_divide_normal (test_calculator.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

Todo método cujo nome começa com test_ é detectado e executado automaticamente. A classe herda de unittest.TestCase, que fornece todos os métodos de asserção.

Referência de métodos de asserção

unittest.TestCase inclui um conjunto abrangente de métodos de asserção. Cada um produz uma mensagem de falha clara quando a verificação não passa.

Asserções de igualdade e identidade

MethodChecksExample
assertEqual(a, b)a == bself.assertEqual(result, 42)
assertNotEqual(a, b)a != bself.assertNotEqual(result, 0)
assertIs(a, b)a is bself.assertIs(singleton, instance)
assertIsNot(a, b)a is not bself.assertIsNot(obj1, obj2)
assertIsNone(x)x is Noneself.assertIsNone(result)
assertIsNotNone(x)x is not Noneself.assertIsNotNone(user)

Asserções booleanas e de pertencimento

MethodChecksExample
assertTrue(x)bool(x) is Trueself.assertTrue(is_valid)
assertFalse(x)bool(x) is Falseself.assertFalse(has_errors)
assertIn(a, b)a in bself.assertIn("admin", roles)
assertNotIn(a, b)a not in bself.assertNotIn("deleted", status)
assertIsInstance(a, b)isinstance(a, b)self.assertIsInstance(result, dict)

Asserções numéricas e de coleções

MethodChecksExample
assertAlmostEqual(a, b)round(a-b, 7) == 0self.assertAlmostEqual(0.1 + 0.2, 0.3)
assertGreater(a, b)a > bself.assertGreater(len(results), 0)
assertLess(a, b)a < bself.assertLess(latency, 1.0)
assertCountEqual(a, b)Mesmos elementos, qualquer ordemself.assertCountEqual([3,1,2], [1,2,3])
assertListEqual(a, b)Listas são iguaisself.assertListEqual(result, expected)
assertDictEqual(a, b)Dicts são iguaisself.assertDictEqual(config, defaults)

Asserções de exceção e warning

import unittest
import warnings
 
class TestExceptions(unittest.TestCase):
 
    def test_raises_value_error(self):
        """assertRaises checks that the exception is raised."""
        with self.assertRaises(ValueError):
            int("not_a_number")
 
    def test_raises_with_message(self):
        """assertRaisesRegex checks both the exception and its message."""
        with self.assertRaisesRegex(ValueError, "invalid literal"):
            int("not_a_number")
 
    def test_warns_deprecation(self):
        """assertWarns checks that a warning is issued."""
        with self.assertWarns(DeprecationWarning):
            warnings.warn("old function", DeprecationWarning)

Sempre prefira asserções específicas em vez de assertTrue. Em vez de self.assertTrue(result == 42), use self.assertEqual(result, 42). A versão específica produz uma mensagem de falha clara como 42 != 41, enquanto assertTrue apenas diz False is not true.

setUp e tearDown: fixtures de teste

A maioria dos testes precisa de algum estado inicial — uma conexão com banco, um arquivo temporário ou um objeto pré-configurado. Os métodos setUp e tearDown rodam antes e depois de cada método de teste, dando a cada teste um ponto de partida “limpo”.

import unittest
import os
import tempfile
 
class TestFileProcessor(unittest.TestCase):
 
    def setUp(self):
        """Runs before EACH test method."""
        self.test_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.test_dir, "data.txt")
        with open(self.test_file, "w") as f:
            f.write("line1\nline2\nline3\n")
 
    def tearDown(self):
        """Runs after EACH test method."""
        os.remove(self.test_file)
        os.rmdir(self.test_dir)
 
    def test_read_lines(self):
        with open(self.test_file, "r") as f:
            lines = f.readlines()
        self.assertEqual(len(lines), 3)
 
    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))
 
    def test_first_line_content(self):
        with open(self.test_file, "r") as f:
            first_line = f.readline().strip()
        self.assertEqual(first_line, "line1")

Cada método de teste recebe sua própria chamada de setUp. Se test_read_lines modificar o arquivo, test_first_line_content ainda verá o conteúdo original porque o setUp o recria.

setUpClass e tearDownClass: setup único

Alguns recursos são caros de criar — conexões de banco, grandes fixtures de dados, processos de servidor. Use setUpClass e tearDownClass para criá-los uma vez para a classe de teste inteira.

import unittest
import sqlite3
 
class TestDatabase(unittest.TestCase):
 
    @classmethod
    def setUpClass(cls):
        """Runs ONCE before all tests in this class."""
        cls.conn = sqlite3.connect(":memory:")
        cls.cursor = cls.conn.cursor()
        cls.cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL
            )
        """)
        cls.cursor.executemany(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            [
                ("Alice", "alice@example.com"),
                ("Bob", "bob@example.com"),
                ("Charlie", "charlie@example.com"),
            ],
        )
        cls.conn.commit()
 
    @classmethod
    def tearDownClass(cls):
        """Runs ONCE after all tests in this class."""
        cls.conn.close()
 
    def test_user_count(self):
        self.cursor.execute("SELECT COUNT(*) FROM users")
        count = self.cursor.fetchone()[0]
        self.assertEqual(count, 3)
 
    def test_find_user_by_email(self):
        self.cursor.execute(
            "SELECT name FROM users WHERE email = ?",
            ("bob@example.com",),
        )
        name = self.cursor.fetchone()[0]
        self.assertEqual(name, "Bob")
 
    def test_unique_emails(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self.cursor.execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                ("Duplicate", "alice@example.com"),
            )
HookRunsDecoratorUse Case
setUpAntes de cada método de testeNoneCriar objetos “do zero”, resetar estado
tearDownDepois de cada método de testeNoneLimpar arquivos, resetar mocks
setUpClassUma vez antes de todos os testes da classe@classmethodConexões de banco, fixtures caras
tearDownClassUma vez depois de todos os testes da classe@classmethodFechar conexões, deletar recursos compartilhados

Mocking com unittest.mock

Aplicações reais dependem de bancos, APIs, sistema de arquivos e serviços de rede. Você não quer que seus testes unitários chamem uma API de produção ou exijam um banco rodando. unittest.mock substitui essas dependências por substitutos controlados.

Objeto Mock básico

from unittest.mock import Mock
 
# Create a mock object
api_client = Mock()
 
# Configure return values
api_client.get_user.return_value = {"id": 1, "name": "Alice"}
 
# Use it like a real object
user = api_client.get_user(user_id=1)
print(user)  # {'id': 1, 'name': 'Alice'}
 
# Verify it was called correctly
api_client.get_user.assert_called_once_with(user_id=1)

Patching com @patch

O decorator @patch substitui um objeto em um módulo específico durante a execução de um teste. Essa é a ferramenta principal para isolar unidades de suas dependências.

# user_service.py
import requests
 
def get_user_name(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()["name"]
# test_user_service.py
import unittest
from unittest.mock import patch, Mock
from user_service import get_user_name
 
class TestUserService(unittest.TestCase):
 
    @patch("user_service.requests.get")
    def test_get_user_name_success(self, mock_get):
        """Mock the requests.get call to avoid hitting the real API."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "Alice"}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
 
        result = get_user_name(1)
 
        self.assertEqual(result, "Alice")
        mock_get.assert_called_once_with("https://api.example.com/users/1")
 
    @patch("user_service.requests.get")
    def test_get_user_name_http_error(self, mock_get):
        """Verify that HTTP errors propagate correctly."""
        import requests
        mock_get.return_value.raise_for_status.side_effect = (
            requests.exceptions.HTTPError("404 Not Found")
        )
 
        with self.assertRaises(requests.exceptions.HTTPError):
            get_user_name(999)

Patching como context manager

import unittest
from unittest.mock import patch
 
class TestConfig(unittest.TestCase):
 
    def test_reads_environment_variable(self):
        with patch.dict("os.environ", {"DATABASE_URL": "sqlite:///test.db"}):
            import os
            self.assertEqual(os.environ["DATABASE_URL"], "sqlite:///test.db")

MagicMock vs Mock

MagicMock é uma subclasse de Mock que vem pré-configurada com métodos mágicos (__len__, __iter__, __getitem__, etc.). Use MagicMock quando o código sob teste chama métodos dunder no objeto mockado.

from unittest.mock import MagicMock
 
mock_list = MagicMock()
mock_list.__len__.return_value = 5
mock_list.__getitem__.return_value = "item"
 
print(len(mock_list))     # 5
print(mock_list[0])       # item

side_effect para comportamento complexo

side_effect permite que um mock lance exceções, retorne valores diferentes em chamadas sucessivas ou execute uma função personalizada.

from unittest.mock import Mock
 
# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database unreachable")
 
# Return different values on successive calls
mock_api = Mock()
mock_api.fetch.side_effect = [{"page": 1}, {"page": 2}, StopIteration]
 
print(mock_api.fetch())  # {'page': 1}
print(mock_api.fetch())  # {'page': 2}
 
# Custom function
def validate_input(x):
    if x < 0:
        raise ValueError("Negative input")
    return x * 2
 
mock_fn = Mock(side_effect=validate_input)
print(mock_fn(5))  # 10

Descoberta de testes (Test Discovery)

Você não precisa listar manualmente cada arquivo de teste. A descoberta de testes do Python encontra e executa todos os testes que seguem a convenção de nomenclatura.

# Discover and run all tests in the current directory tree
python -m unittest discover
 
# Specify a start directory and pattern
python -m unittest discover -s tests -p "test_*.py"
 
# Verbose output
python -m unittest discover -v

A descoberta de testes procura arquivos que correspondem a test_*.py (padrão), importa-os e executa qualquer classe que herde de unittest.TestCase.

Estrutura de projeto recomendada

my_project/
    src/
        calculator.py
        user_service.py
        utils.py
    tests/
        __init__.py
        test_calculator.py
        test_user_service.py
        test_utils.py
    setup.py

Execute todos os testes a partir da raiz do projeto:

python -m unittest discover -s tests -v

Organizando testes com Test Suites

Para controle refinado sobre quais testes rodam, monte suites de teste manualmente.

import unittest
from test_calculator import TestCalculator
from test_user_service import TestUserService
 
def fast_tests():
    """Suite of tests that run quickly (no I/O, no network)."""
    suite = unittest.TestSuite()
    suite.addTest(TestCalculator("test_add_positive_numbers"))
    suite.addTest(TestCalculator("test_add_negative_numbers"))
    suite.addTest(TestCalculator("test_divide_normal"))
    return suite
 
def all_tests():
    """Full test suite."""
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromTestCase(TestCalculator))
    suite.addTests(loader.loadTestsFromTestCase(TestUserService))
    return suite
 
if __name__ == "__main__":
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(fast_tests())

Pulando testes e falhas esperadas

Às vezes, um teste só deve rodar em certas condições — um SO específico, uma versão do Python, ou quando um serviço externo está disponível.

import unittest
import sys
 
class TestPlatformSpecific(unittest.TestCase):
 
    @unittest.skip("Temporarily disabled while refactoring")
    def test_feature_under_construction(self):
        pass
 
    @unittest.skipIf(sys.platform == "win32", "Not supported on Windows")
    def test_unix_permissions(self):
        import os
        self.assertTrue(os.access("/tmp", os.W_OK))
 
    @unittest.skipUnless(sys.platform.startswith("linux"), "Linux only")
    def test_proc_filesystem(self):
        import os
        self.assertTrue(os.path.exists("/proc"))
 
    @unittest.expectedFailure
    def test_known_bug(self):
        """This test documents a known bug. It SHOULD fail."""
        self.assertEqual(1 + 1, 3)
DecoratorEffect
@unittest.skip(reason)Sempre pula este teste
@unittest.skipIf(condition, reason)Pula se a condição for True
@unittest.skipUnless(condition, reason)Pula a menos que a condição seja True
@unittest.expectedFailureMarca como falha esperada; reporta como erro se passar

Testes parametrizados com subTest

Testar a mesma lógica com entradas diferentes é comum. Em vez de criar métodos separados para cada caso, use subTest para executar asserções parametrizadas dentro de um único método.

import unittest
 
def is_palindrome(s):
    cleaned = s.lower().replace(" ", "")
    return cleaned == cleaned[::-1]
 
class TestPalindrome(unittest.TestCase):
 
    def test_palindromes(self):
        test_cases = [
            ("racecar", True),
            ("hello", False),
            ("A man a plan a canal Panama", True),
            ("", True),
            ("ab", False),
            ("madam", True),
            ("Nurses Run", True),
        ]
        for text, expected in test_cases:
            with self.subTest(text=text):
                self.assertEqual(is_palindrome(text), expected)

Com subTest, uma falha em um caso não interrompe a execução dos demais. A saída identifica exatamente qual subcaso falhou:

FAIL: test_palindromes (test_palindrome.TestPalindrome) (text='Nurses Run')
AssertionError: False != True

Sem subTest, a primeira falha abortaria o método inteiro e você não saberia quais outros casos também falham.

unittest vs pytest vs doctest

Python tem três ferramentas de teste embutidas ou comumente usadas. Cada uma atende a um propósito diferente.

Featureunittestpytestdoctest
Included in stdlibYesNo (pip install)Yes
Test styleClass-based (TestCase)Function-based (plain def test_)Embedded in docstrings
Assertionsself.assertEqual, self.assertTrue, etc.Plain assert statementExpected output matching
FixturessetUp/tearDown, setUpClass@pytest.fixture with dependency injectionNone
Parameterized testssubTest (limited)@pytest.mark.parametrize (powerful)One example per docstring
Mockingunittest.mock (built-in)unittest.mock + monkeypatchNot applicable
Test discoverypython -m unittest discoverpytest (auto-discovers)python -m doctest module.py
Output on failureBasic diffDetailed diff with contextShows expected vs actual output
PluginsNone1000+ plugins (coverage, fixtures, etc.)None
Learning curveModerate (OOP patterns)Low (plain functions)Very low
Best forStandard library projects, no extra dependenciesMost Python projects, complex test setupsSimple examples in documentation

Quando escolher unittest:

  • Você quer zero dependências externas
  • Sua organização ou projeto já usa unittest
  • Você precisa de organização de testes baseada em classes
  • Você quer unittest.mock sem configuração extra

Quando escolher pytest:

  • Você quer sintaxe mais simples e melhor saída em falhas
  • Você precisa de parametrização poderosa ou fixtures
  • Você depende do ecossistema de plugins do pytest (coverage, async, Django etc.)

Quando escolher doctest:

  • Você quer verificar que exemplos de código na documentação ainda funcionam
  • Os testes são pares simples de entrada/saída

Observe que o pytest pode executar testes no estilo unittest sem modificação. Muitas equipes começam com unittest e migram para pytest como runner mantendo suas classes de teste existentes.

Testando uma classe do mundo real: exemplo completo

Aqui vai um exemplo completo testando uma implementação de carrinho de compras.

# cart.py
class Product:
    def __init__(self, name, price):
        if price < 0:
            raise ValueError("Price cannot be negative")
        self.name = name
        self.price = price
 
class ShoppingCart:
    def __init__(self):
        self.items = []
 
    def add(self, product, quantity=1):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        self.items.append({"product": product, "quantity": quantity})
 
    def total(self):
        return sum(
            item["product"].price * item["quantity"]
            for item in self.items
        )
 
    def remove(self, product_name):
        self.items = [
            item for item in self.items
            if item["product"].name != product_name
        ]
 
    def item_count(self):
        return sum(item["quantity"] for item in self.items)
# test_cart.py
import unittest
from cart import Product, ShoppingCart
 
class TestProduct(unittest.TestCase):
 
    def test_create_product(self):
        p = Product("Widget", 9.99)
        self.assertEqual(p.name, "Widget")
        self.assertAlmostEqual(p.price, 9.99)
 
    def test_negative_price_raises(self):
        with self.assertRaises(ValueError):
            Product("Bad", -5.00)
 
class TestShoppingCart(unittest.TestCase):
 
    def setUp(self):
        self.cart = ShoppingCart()
        self.apple = Product("Apple", 1.50)
        self.bread = Product("Bread", 3.00)
 
    def test_empty_cart_total(self):
        self.assertEqual(self.cart.total(), 0)
 
    def test_add_single_item(self):
        self.cart.add(self.apple)
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 1.50)
 
    def test_add_multiple_items(self):
        self.cart.add(self.apple, quantity=3)
        self.cart.add(self.bread, quantity=2)
        self.assertEqual(self.cart.item_count(), 5)
        self.assertAlmostEqual(self.cart.total(), 10.50)
 
    def test_remove_item(self):
        self.cart.add(self.apple, quantity=2)
        self.cart.add(self.bread)
        self.cart.remove("Apple")
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 3.00)
 
    def test_remove_nonexistent_item(self):
        self.cart.add(self.apple)
        self.cart.remove("Nonexistent")
        self.assertEqual(self.cart.item_count(), 1)
 
    def test_add_zero_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=0)
 
    def test_add_negative_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=-1)
 
if __name__ == "__main__":
    unittest.main()

Boas práticas para unittest

1. Uma asserção por conceito

Cada método de teste deve verificar um conceito lógico. Várias asserções são ok se todas verificarem aspectos diferentes da mesma operação.

# GOOD -- multiple assertions about the same operation
def test_user_creation(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    self.assertEqual(user.email, "alice@example.com")
    self.assertIsNotNone(user.id)
 
# BAD -- testing unrelated things in one method
def test_everything(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    users = list_all_users()
    self.assertGreater(len(users), 0)  # Unrelated assertion

2. Use nomes de teste descritivos

Nomes de teste devem descrever o cenário e o resultado esperado. Quando um teste falha no CI, o nome por si só deve indicar o que deu errado.

# GOOD
def test_divide_by_zero_raises_value_error(self):
    ...
 
def test_empty_cart_returns_zero_total(self):
    ...
 
# BAD
def test_divide(self):
    ...
 
def test1(self):
    ...

3. Os testes devem ser independentes

Nenhum teste deve depender da saída de outro teste ou da ordem de execução. Cada teste deve montar seu próprio estado e limpar depois.

4. Mantenha os testes rápidos

Testes unitários devem rodar em milissegundos. Se um teste precisa de banco, faça mock. Se precisa de API, faça mock. Deixe testes de integração lentos para uma suite separada.

5. Teste edge cases

Sempre teste condições de limite: entradas vazias, valores zero, None, entradas muito grandes e tipos inválidos.

def test_edge_cases(self):
    test_cases = [
        ([], 0),          # empty list
        ([0], 0),         # single zero
        ([-1, -2], -3),   # all negative
        ([999999999], 999999999),  # large number
    ]
    for inputs, expected in test_cases:
        with self.subTest(inputs=inputs):
            self.assertEqual(sum(inputs), expected)

6. Não teste detalhes de implementação

Teste a interface pública e o comportamento, não o estado interno ou métodos privados. Se você refatorar internamente, seus testes ainda devem passar.

Rodando testes pela linha de comando

# Run a specific test file
python -m unittest test_calculator.py
 
# Run a specific test class
python -m unittest test_calculator.TestCalculator
 
# Run a specific test method
python -m unittest test_calculator.TestCalculator.test_add_positive_numbers
 
# Verbose output (shows each test name and result)
python -m unittest -v
 
# Discover all tests in a directory
python -m unittest discover -s tests -p "test_*.py" -v
 
# Stop on first failure (fail-fast mode)
python -m unittest -f

Escrevendo e depurando testes com RunCell

Quando você desenvolve em notebooks Jupyter — algo comum em ciência de dados e programação exploratória — escrever e executar testes unitários parece estranho. Notebooks executam células de forma interativa, mas o unittest espera módulos e runners de teste. Você acaba copiando código entre notebooks e arquivos de teste, perdendo o ciclo de feedback interativo.

RunCell (opens in a new tab) é um agente de IA projetado para Jupyter que preenche essa lacuna. Ele pode gerar casos de teste compatíveis com unittest para funções que você define em células do notebook, executá-los dentro do ambiente do notebook e explicar falhas com contexto. Se um mock estiver configurado incorretamente ou uma asserção falhar, o RunCell inspeciona as variáveis “ao vivo” e mostra quais eram os valores reais — não apenas a mensagem de erro da asserção. Para pipelines de dados em que você precisa verificar se transformações de DataFrame produzem a forma e os valores corretos, o RunCell pode criar a estrutura de teste e as asserções, para que você foque na lógica em vez de boilerplate.

FAQ

Qual é a diferença entre unittest e pytest?

unittest é o framework de testes embutido do Python com uma API baseada em classes. pytest é um framework de terceiros que usa funções simples e assert. O pytest tem um ecossistema de plugins mais rico e uma saída melhor em falhas, mas exige instalação. O unittest funciona em qualquer lugar em que o Python roda, sem dependências extras.

Como eu executo um único método de teste no unittest?

Use o comando python -m unittest test_module.TestClass.test_method. Por exemplo: python -m unittest test_calculator.TestCalculator.test_add_positive_numbers. Isso executa apenas o método especificado sem rodar os outros testes do arquivo.

Qual é a diferença entre setUp e setUpClass?

setUp roda antes de cada método de teste individual, criando um estado novo a cada vez. setUpClass roda uma vez antes de todos os testes da classe e é um @classmethod. Use setUpClass para configurações caras, como conexões de banco. Use setUp para estado leve por teste.

Como eu faço mock de uma API externa no unittest?

Use unittest.mock.patch para substituir a função que chama a API. Faça patch no caminho de import onde a função é usada, não onde ela é definida. Por exemplo, se user_service.py importa requests.get, faça patch em user_service.requests.get, não em requests.get.

O pytest consegue rodar testes unittest?

Sim. O pytest é totalmente compatível com classes de teste no estilo unittest. Você pode rodar pytest em um projeto que usa classes unittest.TestCase sem nenhuma modificação. Isso torna a migração gradual — você pode escrever novos testes no estilo pytest enquanto mantém testes existentes do unittest.

Como eu testo que uma função lança uma exceção?

Use self.assertRaises(ExceptionType) como context manager. O teste passa se o código dentro do bloco with lançar a exceção especificada e falha se nenhuma exceção (ou uma diferente) for lançada. Use assertRaisesRegex para também verificar a mensagem da exceção.

Conclusão

O framework unittest do Python é um kit completo de testes que vem com toda instalação do Python. Ele fornece classes de caso de teste, métodos de asserção ricos, hooks de setup e teardown tanto no nível de método quanto de classe, recursos de mocking via unittest.mock e descoberta de testes embutida. Você não precisa instalar nada para começar a escrever testes confiáveis.

Os fundamentos são diretos: herde de TestCase, nomeie seus métodos com o prefixo test_, use métodos de asserção específicos e execute com python -m unittest. À medida que seu projeto cresce, adicione setUp/tearDown para isolamento de testes, @patch para mockar dependências externas e subTest para testes parametrizados. Organize testes em um diretório tests/ e deixe a descoberta de testes cuidar do resto.

Escrever testes leva tempo no começo. Mas economiza muito mais tempo depois. Cada regressão capturada por um teste unitário é um incidente em produção que nunca aconteceu, uma reclamação de cliente que nunca chegou e uma sessão de debugging que nunca começou. Seja você alguém que fica com unittest ou eventualmente migra para pytest, os hábitos de teste que você desenvolve com os padrões do unittest — isolamento, asserções claras, dependências mockadas e cobertura abrangente de edge cases — se aplicam universalmente a testes em Python.

📚