É muito comum programadores inexperientes desrespeitarem contratosestabelecidos, algumas vezes por não saberem como resolver uma situação de
outra forma, outras por
Um dos erros mais comuns que tenho visto no mundo Python é a quebra do contrato da classe TestCase
.
Geralmente a quebra de contrato ocorre porque o programador não entende o conceito de hook ou não sabe como evitar chamar super
na
herança.
Hook é aquele método preenchido pelo código principal para ser chamado por um framework. Por definição, o hook não deve ser chamado no código de aplicação, ou deve ser evitado. Em seu lugar é feita chamada para alguma rotina do framework que, por sua fez, usa o hook dentro da conveniência do mesmo.
Na biblioteca de unit test de Python, os métodos de TestCase
setUp
e tearDown
(de instância), e setUpClass
e
tearDownClass
são métodos hook, evocados pelo framework
de teste unitário.
O programador júnior, muito satisfeito com seu entendimento sobre herança,quer então aplicar o mesmo no máximo de soluções possíveis e encontrou uma abordagem em que a herança se encaixa como uma luva: nos testes unitários, ele precisa iniciar uma sessão com o banco (um SQLite) antes de cada teste, e encerrá-lo ao final de cada um.
Ele sabe que o método setUp
é executado antes de cada teste, enquanto o método tearDown
é executado ao final, havendo erro,
falha ou sucesso. Perfeito!
Mas ele precisa que todas as classes de teste usem esses métodos, então ele pensa numa solução que parece perfeita: implementar os métodos hook numa classe pai, que será herdada pelas demais classes de teste.
A coisa fica mais ou menos assim:
import unittest
import sqlite3
import my_app
__all__ = ['TestCase']
class TestCase(unittest.TestCase):
def setUp(self):
conn = my_app.conn = sqlite3.connect(':memory:')
conn.execute("""CREATE TABLE t_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
birth DATE,
register INTEGER
)""")
conn.commit()
def tearDown(self):
my_app.conn.close()
Tudo ia às mil maravilhas, até nosso programador descobrir que, em um de seustestes, ele precisa simular o comportamento de um acesso a um servidor Redis, o que sobrescreve sua perfeita solução de banco de dados!
Qual a nova solução? O que se faz em herança: evocar super
!
from unittest.mock import patch
from my_test_case import TestCase
from my_app import CacheManager
__all__ = ['TestCacheManager']
class TestCacheManager(TestCase):
def setUp(self):
super().setUp()
redis_patch = self.redis_patch = patch('my_app.Redis')
self.redis = redis_patch.start()
def tearDown(self):
self.redis_patch.stop()
super().tearDown()
"""Seguem os testes..."""
O que nosso resoluto programador júnior não percebeu – ou percebeu, mas não sacou como resolver – é que o contrato do unittest
foi quebrado.
A regra de ouro do unittest
é: jamais implemente métodos hook em classes que não sejam testes de fato, que sejam abstrações de
comportamento para casos de teste de fato.
— Mas como resolver então? 😯
Como sua classe pai tem o objetivo de alterar o comportamento do framework, sobrescreva rotinas internas do framework.
No caso, o método cujo comportamento você quer mudar é run
. A alteração será parecida com a criação de um contexto usando
contextlib.contextmanager
, apenas trocando o yield
por
super
:
class TestCase(unittest.TestCase):
def run(self, result=None):
conn = my_app.conn = sqlite3.connect(':memory:')
conn.execute("""CREATE TABLE t_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
birth DATE,
register INTEGER
)""")
conn.commit()
try:
return super().run(result=result)
finally:
conn.close()
Pronto! O problema já está resolvido! Agora nossa classe TestCase
já possui comportamento de framework, e os hooks podem ser usados
corretamente:
class TestCacheManager(TestCase):
def setUp(self):
redis_patch = self.redis_patch = patch('my_app.Redis')
self.redis = redis_patch.start()
def tearDown(self):
self.redis_patch.stop()
"""Seguem os testes..."""
Esses problemas são causados por desentendimento dos contratos edesconhecimento da linguagem, mas são facilmente resolvidos com um pouco de pesquisa e leitura dos códigos das bibliotecas padrão, que são muito bem documentadas.