A documentação do Pyramid não contempla o jeito preferencial de testar com SQLAlchemy, pois o Pyramid tenta ficar fora do seu caminho e permitir que você tome suas próprias decisões. No entanto, eu acho necessário documentar o que eu penso ser a melhor forma de testar.
Quando eu comecei a escrever testes com o SQLAlchemy, eu descobri muitos exemplos de como começar, fazendo algo como o seguinte:
from db import session # probably a contextbound sessionmaker
from db import model
from sqlalchemy import create_engine
def setup():
engine = create_engine('sqlite:///test.db')
session.configure(bind=engine)
model.metadata.create_all(engine)
def teardown():
model.metadata.drop_all(engine)
def test_something():
pass
Eu já vi isso feito muitas vezes, mas acredito que existem coisas muito erradas nisso! Então vamos estabelecer algumas regras básicas ao testar:
- Sempre teste seu sistema como se ele fosse usado em produção. O SQLite não aplica as mesmas regras ou tem as mesmas características como Postgres ou MySQL e possibilitará testes, que falhariam na produção, passarem.
- Testes devem ser rápidos! Você deve escrever testes para todos o seu código. Essa é a principal razão pela qual as pessoas fazem testes contra SQLite, mas não podemos violar a regra número um. Temos que ter certeza de que testes contra Postgres são rápidos, para que nós descartemos e recriemos tabelas para cada teste.
- Você deve ser capaz de executar em paralelo para acelerar quando você tem vários testes. Criar tabelas para cada teste não irá funcionar em um ambiente paralelo.
Por exemplo, eu tenho um projeto com mais de 600 testes, que levariam 2 minutos e meio para ser executado contra SQLite. Mas quando mudamos nossa configuração de teste para executar contra Postgres, eles levariam mais de uma hora. Isso é inaceitável!
Porém, executá-los em paralelo nos dará uma grande aceleração. Veja os resultados dos testes executando em um único modo proc versus usando todos os 4 núcleos.
$ py.test
======= 616 passed in 143.67 seconds =======
$ py.test -n4
======= 616 passed in 68.12 seconds =======
A maneira certa
Então, qual é a maneira adequada de configurar seus testes? Você deve iniciar o banco de dados quando começar seu executor de testes e então usar transições para reverter quaisquer modificações de informações que seus testes fizerem. Isso permite que você mantenha um banco de dados limpo para cada teste de forma eficiente.
No teste py. você terá que criar somente um arquivo chamado conftest.py, que é parecido com:
import os
ROOT_PATH = os.path.dirname(__file__)
def pytest_sessionstart():
from py.test import config
# Only run database setup on master (in case of xdist/multiproc mode)
if not hasattr(config, 'slaveinput'):
from models import initialize_sql
from pyramid.config import Configurator
from paste.deploy.loadwsgi import appconfig
from sqlalchemy import engine_from_config
import os
ROOT_PATH = os.path.dirname(__file__)
settings = appconfig('config:' + os.path.join(ROOT_PATH, 'test.ini'))
engine = engine_from_config(settings, prefix='sqlalchemy.')
print 'Creating the tables on the test database %s' % engine
config = Configurator(settings=settings)
initialize_sql(settings, config)
Com o teste py. quando você está executando em modo paralelo, a chamada pytest_sessionstart … é ativado para cada nó, então nós checamos que estamos no nó mestre. Então nós pegamos nosso arquivo de configuração text.ini e executamos a função initialize_sql.
Agora que você tem a configuração do seu teste inicial concluída, você tem que definir uma classe de teste base que faz o gerenciamento das transações no setUp e no teardown.
Primeiro, vamos configurar a classe de teste base (?) que irá controlar nossas transações:
import unittest
from pyramid import testing
from paste.deploy.loadwsgi import appconfig
from webtest import TestApp
from mock import Mock
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker
from app.db import Session
from app.db import Entity # base declarative object
from app import main
import os
here = os.path.dirname(__file__)
settings = appconfig('config:' + os.path.join(here, '../../', 'test.ini'))
class BaseTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.engine = engine_from_config(settings, prefix='sqlalchemy.')
cls.Session = sessionmaker()
def setUp(self):
connection = self.engine.connect()
# begin a non-ORM transaction
self.trans = connection.begin()
# bind an individual Session to the connection
Session.configure(bind=connection)
self.session = self.Session(bind=connection)
Entity.session = self.session
def tearDown(self):
# rollback - everything that happened with the
# Session above (including calls to commit())
# is rolled back.
testing.tearDown()
self.trans.rollback()
self.session.close()
Essa hipótese de teste base irá juntar todas as suas sessões em uma transação externa para que você ainda tenha a possibilidade de usar comando flush/commit/etc e ainda será capaz de reverter quaisquer mudanças de informações que você faça.
Unidade de testes
Agora existem alguns diferentes tipos de testes que você vai querer executar. Primeiro você irá querer fazer unidade de testes (?), que são pequenos testes que checam apenas uma coisa de cada vez. Isso significa que você pulará rotas, templates etc. Então vamos configurar nossa classe de Unidade Base de Teste:
class UnitTestBase(BaseTestCase):
def setUp(self):
self.config = testing.setUp(request=testing.DummyRequest())
super(UnitTestBase, self).setUp()
def get_csrf_request(self, post=None):
csrf = 'abc'
if not u'csrf_token' in post.keys():
post.update({
'csrf_token': csrf
})
request = testing.DummyRequest(post)
request.session = Mock()
csrf_token = Mock()
csrf_token.return_value = csrf
request.session.get_csrf_token = csrf_token
return request
Nós construímos uma função utilitária para nos ajudar a testar solicitações que necessitam um símbolo csrf também. Aqui está como usaríamos essa classe:
class TestViews(UnitTestBase):
def test_login_fails_empty(self):
""" Make sure we can't login with empty credentials"""
from app.accounts.views import LoginView
self.config.add_route('index', '/')
self.config.add_route('dashboard', '/')
request = testing.DummyRequest(post={
'submit': True,
})
view = LoginView(request)
response = view.post()
errors = response['errors']
assert errors[0].node.name == u'csrf_token'
assert errors[0].msg == u'Required'
assert errors[1].node.name == u'Username'
assert errors[1].msg == u'Required'
assert errors[2].node.name == u'Password'
assert errors[2].msg == u'Required'
def test_login_succeeds(self):
""" Make sure we can login """
admin = User(username='sontek', password='temp', kind=u'admin')
admin.activated = True
self.session.add(admin)
self.session.flush()
from app.accounts.views import LoginView
self.config.add_route('index', '/')
self.config.add_route('dashboard', '/dashboard')
request = self.get_csrf_request(post={
'submit': True,
'Username': 'sontek',
'Password': 'temp',
})
view = LoginView(request)
response = view.post()
assert response.status_int == 302
Testes de integração
O segundo tipo de teste que você irá querer escrever é um teste de integração. Isso irá integrar todo o framework web e na verdade irá atingir rotas definidas, renderizar os templates e realmente testar o total conteúdo da sua aplicação.
Felizmente, isso é muito fácil de ser feito com Pyramid usando WebTest:
class IntegrationTestBase(BaseTestCase):
@classmethod
def setUpClass(cls):
cls.app = main({}, **settings)
super(IntegrationTestBase, cls).setUpClass()
def setUp(self):
self.app = TestApp(self.app)
self.config = testing.setUp()
super(IntegrationTestBase, self).setUp()
No setUpClass, nós executamos a principal função das aplicações __init__.py que configura o aplicativo WSGI e depois nós juntamos em um TestApp que nós dá a possibilidade de utilizar comandos get/post sobre ele.
Aqui está um exemplo dele em uso:
class TestViews(IntegrationTestBase):
def test_get_login(self):
""" Call the login view, make sure routes are working """
res = self.app.get('/login')
self.assertEqual(res.status_int, 200)
def test_empty_login(self):
""" Empty login fails """
res = self.app.post('/login', {'submit': True})
assert "There was a problem with your submission" in res.body
assert "Required" in res.body
assert res.status_int == 200
def test_valid_login(self):
""" Call the login view, make sure routes are working """
admin = User(username='sontek', password='temp', kind=u'admin')
admin.activated = True
self.session.add(admin)
self.session.flush()
res = self.app.get('/login')
csrf = res.form.fields['csrf_token'][0].value
res = self.app.post('/login',
{
'submit': True,
'Username': 'sontek',
'Password': 'temp',
'csrf_token': csrf
}
)
assert res.status_int == 302
Problemas com esta abordagem
Se um teste causa um erro que irá prevenir que a transação seja revertida, como fechar o mecanismo, então essa abordagem irá deixar seu banco de dados em um estado que poderá causar a falha de outros testes.
Se isso ocorrer, rastrear a rota da causa poderá ser difícil, mas você deverá ser capaz de olhar somente o primeiro teste que falhou, a não ser que esteja executando testes em paralelo.
Se você for bom em escrever e executar seus testes regularmente, você deverá ser capaz de capturar testes individuais, tornando problemas como esses passageiros.
?
Texto original disponível em http://sontek.net/writing-tests-for-pyramid-and-sqlalchemy