I've minimized my problem to a self-contained flask app + unit test. When this is run with pytest app.py it fails roughly half the time (29 out of 50 runs) with this error:
E werkzeug.routing.BuildError: Could not build url for endpoint 'thing' with values ['_sa_instance_state']. Did you forget to specify values ['id']?
The frustrating part about this is that adding debugging statements in the post() method make it always pass (see comment below).
This feels like a race condition somewhere in the framework. Is SQLAlchemy spawning a thread to perform the commit and update t.id?
I can force it to fail by doing a del t.id at the site of the comment (confirming that the error is coming from a missing t.id). I can force it to pass by doing a t.id = 999 at the same spot.
Am I doing something obviously wrong here or is this a bug in one of the packages?
I'm running python 3.5.2 and my requirements.txt is:
Flask==1.0.2
Flask-RESTful==0.3.6
Flask-SQLAlchemy==2.3.2
Jinja2==2.10
pytest==3.2.2
pytest-repeat==0.4.1
SQLAlchemy==1.2.8
Werkzeug==0.14.1
It may be worth noting that this also failed with earlier versions of most of these packages (flask 0.12, sqlalchemy 1.1.14, etc).
It may also be worth noting that when run with pytest --count=20 app.py it will always pass or fail the entire count, i.e. 20 passes or 20 failures. But about half the overall runs will still fail.
Here's the app:
#!/usr/bin/env python3
import json
from flask import Flask
from flask_restful import Api, Resource, fields, marshal, reqparse
from flask_sqlalchemy import SQLAlchemy
import pytest
app = Flask(__name__)
api = Api(app)
db = SQLAlchemy(app)
class Thing(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
thing_fields = {
'name': fields.String,
'uri': fields.Url('thing'),
}
class ThingListAPI(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
self.reqparse.add_argument('name', type=str, location='json')
super().__init__()
def post(self):
args = self.reqparse.parse_args()
t = Thing(name=args['name'])
db.session.add(t)
db.session.commit()
### <<< at this point inserting pretty much any statement
### will make the test pass >>>
return {'thing': marshal(t, thing_fields)}, 201
class ThingAPI(Resource):
def get(self, id):
pass
api.add_resource(ThingListAPI, '/things', endpoint='things')
api.add_resource(ThingAPI, '/things/<int:id>', endpoint='thing')
#pytest.fixture
def stub_app():
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
client = app.test_client()
db.create_all()
yield client
db.drop_all()
def test_thing_post(stub_app):
resp = stub_app.post('/things', data=json.dumps({'name': 'stuff'}),
content_type='application/json')
assert(resp.status_code == 201)
If you add db.session.refresh(t) in post() after commit, it will solve the problem. I don't know whether it's a right thing to do so (SQLAlchemy is quite complicated and I had a little experience with it), but it shows that a t-object state sometimes is not refreshed (sometimes because probably there is a race condition and sometimes SQLAlchemy maybe gets more machine time and pulls id just in time, but sometimes is not) after a commit and id-attribute still somehow doesn't exist (I mean, for flask, because for sqlite it does exist, but a new state is not pulled from a DB).
Related
I'm looking for a way to be able to test code using pytest as well as use that code in production, and I'm struggling with session handling.
For pytest, I have a conftest.py that includes:
#pytest.fixture
def session(setup_database, connection):
transaction = connection.begin()
yield scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=connection)
)
transaction.rollback()
That allows me to write low-level tests using a test database along the lines of:
def test_create(session):
thing = Things(session, "my thing")
assert thing
...where Things is a sqlalchemy declarative base class defining a database table. This works fine.
The problem I'm trying to solve arises when testing higher levels of the code. The models.py includes:
engine = sqlalchemy.create_engine(
Config.MYSQL_CONNECT,
encoding='utf-8',
pool_pre_ping=True)
Session = scoped_session(sessionmaker(bind=engine))
...and the usage in the code is typically:
def fn():
with Session() as session:
thing = Things(session, "my thing")
I want fn() to use the Session defined in models.py in production, but use the pytest Session in testing.
I clearly have this architected incorrectly but I'm struggling to find a way forwards for what must be quite a common problem.
How do others handle this?
Currently I'm doing the following to get the current running app
async def handler(request):
app = request.app
Isn't there another way for getting the current running app? Consider the below snippet (the default for author_id):
class Comment(DeclarativeBase):
author_id = Column(Integer, ForeignKey('member.id'), default=Member.current_logged_in())
class Member(DeclarativeBase):
#classmethod
def current_logged_in()
pass
As the session is kept in the current running app and as you can see it is only accessible from the incoming request, how can I get the current running app to use the session for determining the current_logged_in user and thus be used as the default value for Comment's author_id?
I wish I have made my point.
Right now there is no implicit context for aiohttp application.
BTW don't do synchronous calls (SQLAlchemy ORM in your case) from aiohttp code.
When running my tests I am getting the following traceback.
in get_context_variable
raise RuntimeError("Signals not supported")
RuntimeError: Signals not supported
__init__.py
from flask_testing import TestCase
from app import create_app, db
class BaseTest(TestCase):
BASE_URL = 'http://localhost:5000/'
def create_app(self):
return create_app('testing')
def setUp(self):
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
def test_setup(self):
response = self.client.get(self.BASE_URL)
self.assertEqual(response.status_code, 200)
test_routes.py
from . import BaseTest
class TestMain(BaseTest):
def test_empty_index(self):
r = self.client.get('/')
self.assert200(r)
self.assertEqual(self.get_context_variable('partners'), None)
It appears that the get_context_variable function call is where the error is coming from. I also receive this error if I try and use assert_template_used. Having a rather difficult time finding any resolution to this.
Flask only provides signals as an optional dependency. Flask-Testing requires signals in some places and raises an error if you try to do something without them. For some reason, some messages are more vague than others Flask-Testing raises elsewhere. (This is a good place for a beginner to contribute a pull request.)
You need to install the blinker library to enable signal support in Flask.
$ pip install blinker
I am developing my API server with Python-eve, and would like to know how to test the API endpoints. A few things that I would like to test specifically:
Validation of POST/PATCH requests
Authentication of different endpoints
Before_ and after_ hooks working property
Returning correct JSON response
Currently I am testing the app against a real MongoDB, and I can imagine the testing will take a long time to run once I have hundreds or thousands of tests to run. Mocking up stuff is another approach but I couldn't find tools that allow me to do that while keeping the tests as realistic as possible. I am wondering if there is a recommended way to test eve apps. Thanks!
Here is what I am having now:
from pymongo import MongoClient
from myModule import create_app
import unittest, json
class ClientAppsTests(unittest.TestCase):
def setUp(self):
app = create_app()
app.config['TESTING'] = True
self.app = app.test_client()
# Insert some fake data
client = MongoClient(app.config['MONGO_HOST'], app.config['MONGO_PORT'])
self.db = client[app.config['MONGO_DBNAME']]
new_app = {
'client_id' : 'test',
'client_secret' : 'secret',
'token' : 'token'
}
self.db.client_apps.insert(new_app)
def tearDown(self):
self.db.client_apps.remove()
def test_access_public_token(self):
res = self.app.get('/token')
assert res.status_code == 200
def test_get_token(self):
query = { 'client_id': 'test', 'client_secret': 'secret' }
res = self.app.get('/token', query_string=query)
res_obj = json.loads(res.get_data())
assert res_obj['token'] == 'token'
The Eve test suite itself is using a test db and not mocking anything. The test db gets created and dropped on every run to guarantee isolation (not super fast yes, but as close as possible to a production environment). While of course you should test your own code, you probably don't need to write tests like test_access_public_token above since, stuff like that is covered by the Eve suite already. You might want to check the Eve Mocker extension too.
Also make yourself familiar with Authentication and Authorization tutorials. It looks like the way you're going get the whole token thing going is not really appropriate (you want to use request headers for that kind of stuff).
When I run py.test --with-gae, I get the following error (I have pytest_gae plugin installed):
def get_current_session():
"""Returns the session associated with the current request."""
> return _tls.current_session
E AttributeError: 'thread._local' object has no attribute 'current_session'
gaesessions/__init__.py:50: AttributeError
I'm using pytest to test my google appengine application. The application runs fine when run in the localhost SDK or when deployed to GAE servers. I just can't figure out how to make pytest work with gaesessions.
My code is below:
test_handlers.py
from webtest import TestApp
import appengine_config
def pytest_funcarg__anon_user(request):
from main import app
app = appengine_config.webapp_add_wsgi_middleware(app)
return TestApp(app)
def test_session(anon_user):
from gaesessions import get_current_session
assert get_current_session()
appengine_config.py
from gaesessions import SessionMiddleware
def webapp_add_wsgi_middleware(app):
from google.appengine.ext.appstats import recording
app = recording.appstats_wsgi_middleware(app)
app = SessionMiddleware(app, cookie_key="replaced-with-this-boring-text")
return app
Relevant code from gaesessions:
# ... more code are not show here ...
_tls = threading.local()
def get_current_session():
"""Returns the session associated with the current request."""
return _tls.current_session
# ... more code are not show here ...
class SessionMiddleware(object):
"""WSGI middleware that adds session support.
``cookie_key`` - A key used to secure cookies so users cannot modify their
content. Keys should be at least 32 bytes (RFC2104). Tip: generate your
key using ``os.urandom(64)`` but do this OFFLINE and copy/paste the output
into a string which you pass in as ``cookie_key``. If you use ``os.urandom()``
to dynamically generate your key at runtime then any existing sessions will
become junk every time your app starts up!
``lifetime`` - ``datetime.timedelta`` that specifies how long a session may last. Defaults to 7 days.
``no_datastore`` - By default all writes also go to the datastore in case
memcache is lost. Set to True to never use the datastore. This improves
write performance but sessions may be occassionally lost.
``cookie_only_threshold`` - A size in bytes. If session data is less than this
threshold, then session data is kept only in a secure cookie. This avoids
memcache/datastore latency which is critical for small sessions. Larger
sessions are kept in memcache+datastore instead. Defaults to 10KB.
"""
def __init__(self, app, cookie_key, lifetime=DEFAULT_LIFETIME, no_datastore=False, cookie_only_threshold=DEFAULT_COOKIE_ONLY_THRESH):
self.app = app
self.lifetime = lifetime
self.no_datastore = no_datastore
self.cookie_only_thresh = cookie_only_threshold
self.cookie_key = cookie_key
if not self.cookie_key:
raise ValueError("cookie_key MUST be specified")
if len(self.cookie_key) < 32:
raise ValueError("RFC2104 recommends you use at least a 32 character key. Try os.urandom(64) to make a key.")
def __call__(self, environ, start_response):
# initialize a session for the current user
_tls.current_session = Session(lifetime=self.lifetime, no_datastore=self.no_datastore, cookie_only_threshold=self.cookie_only_thresh, cookie_key=self.cookie_key)
# create a hook for us to insert a cookie into the response headers
def my_start_response(status, headers, exc_info=None):
_tls.current_session.save() # store the session if it was changed
for ch in _tls.current_session.make_cookie_headers():
headers.append(('Set-Cookie', ch))
return start_response(status, headers, exc_info)
# let the app do its thing
return self.app(environ, my_start_response)
The problem is that your gae sessions is not yet called until the app is also called. The app is only called when you make a request to it. Try inserting a request call before you check for the session value. Check out the revised test_handlers.py code below.
def test_session(anon_user):
anon_user.get("/") # get any url to call the app to create a session.
from gaesessions import get_current_session
assert get_current_session()