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 -vSaí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
OKTodo 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
| Method | Checks | Example |
|---|---|---|
assertEqual(a, b) | a == b | self.assertEqual(result, 42) |
assertNotEqual(a, b) | a != b | self.assertNotEqual(result, 0) |
assertIs(a, b) | a is b | self.assertIs(singleton, instance) |
assertIsNot(a, b) | a is not b | self.assertIsNot(obj1, obj2) |
assertIsNone(x) | x is None | self.assertIsNone(result) |
assertIsNotNone(x) | x is not None | self.assertIsNotNone(user) |
Asserções booleanas e de pertencimento
| Method | Checks | Example |
|---|---|---|
assertTrue(x) | bool(x) is True | self.assertTrue(is_valid) |
assertFalse(x) | bool(x) is False | self.assertFalse(has_errors) |
assertIn(a, b) | a in b | self.assertIn("admin", roles) |
assertNotIn(a, b) | a not in b | self.assertNotIn("deleted", status) |
assertIsInstance(a, b) | isinstance(a, b) | self.assertIsInstance(result, dict) |
Asserções numéricas e de coleções
| Method | Checks | Example |
|---|---|---|
assertAlmostEqual(a, b) | round(a-b, 7) == 0 | self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertGreater(a, b) | a > b | self.assertGreater(len(results), 0) |
assertLess(a, b) | a < b | self.assertLess(latency, 1.0) |
assertCountEqual(a, b) | Mesmos elementos, qualquer ordem | self.assertCountEqual([3,1,2], [1,2,3]) |
assertListEqual(a, b) | Listas são iguais | self.assertListEqual(result, expected) |
assertDictEqual(a, b) | Dicts são iguais | self.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"),
)| Hook | Runs | Decorator | Use Case |
|---|---|---|---|
setUp | Antes de cada método de teste | None | Criar objetos “do zero”, resetar estado |
tearDown | Depois de cada método de teste | None | Limpar arquivos, resetar mocks |
setUpClass | Uma vez antes de todos os testes da classe | @classmethod | Conexões de banco, fixtures caras |
tearDownClass | Uma vez depois de todos os testes da classe | @classmethod | Fechar 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]) # itemside_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)) # 10Descoberta 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 -vA 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.pyExecute todos os testes a partir da raiz do projeto:
python -m unittest discover -s tests -vOrganizando 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)| Decorator | Effect |
|---|---|
@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.expectedFailure | Marca 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 != TrueSem 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.
| Feature | unittest | pytest | doctest |
|---|---|---|---|
| Included in stdlib | Yes | No (pip install) | Yes |
| Test style | Class-based (TestCase) | Function-based (plain def test_) | Embedded in docstrings |
| Assertions | self.assertEqual, self.assertTrue, etc. | Plain assert statement | Expected output matching |
| Fixtures | setUp/tearDown, setUpClass | @pytest.fixture with dependency injection | None |
| Parameterized tests | subTest (limited) | @pytest.mark.parametrize (powerful) | One example per docstring |
| Mocking | unittest.mock (built-in) | unittest.mock + monkeypatch | Not applicable |
| Test discovery | python -m unittest discover | pytest (auto-discovers) | python -m doctest module.py |
| Output on failure | Basic diff | Detailed diff with context | Shows expected vs actual output |
| Plugins | None | 1000+ plugins (coverage, fixtures, etc.) | None |
| Learning curve | Moderate (OOP patterns) | Low (plain functions) | Very low |
| Best for | Standard library projects, no extra dependencies | Most Python projects, complex test setups | Simple 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.mocksem 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 assertion2. 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 -fEscrevendo 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.