Testing Tornado app for 4xx status code - python

Consider the following Tornado (v 4.0.2) application, which is a little bit modified version of official hello world example:
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.set_status(400)
self.write("Hello, world")
application = tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
As you can see, the only difference here is set_status call in MainHandler. Now, I save this code into app.py, then I open tests.py and I put there this simple unit test:
import tornado.ioloop
from tornado.httpclient import HTTPRequest
from tornado.testing import AsyncHTTPTestCase, gen_test
from app import application
class SimpleTest(AsyncHTTPTestCase):
def get_app(self):
return application
def get_new_ioloop(self):
return tornado.ioloop.IOLoop.instance()
#gen_test
def test_bad_request(self):
request = HTTPRequest(url=self.get_url('/'))
response = yield self.http_client.fetch(request)
self.assertEqual(response.code, 400)
When I run this test with python -m tornado.test.runtests tests I get the following result:
E
======================================================================
ERROR: test_bad_request (tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/lib/python2.7/dist-packages/tornado/testing.py", line 118, in __call__
result = self.orig_method(*args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/tornado/testing.py", line 494, in post_coroutine
timeout=timeout)
File "/usr/local/lib/python2.7/dist-packages/tornado/ioloop.py", line 418, in run_sync
return future_cell[0].result()
File "/usr/local/lib/python2.7/dist-packages/tornado/concurrent.py", line 109, in result
raise_exc_info(self._exc_info)
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 631, in run
yielded = self.gen.throw(*sys.exc_info())
File "tests.py", line 18, in test_bad_request
response = yield self.http_client.fetch(request)
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 628, in run
value = future.result()
File "/usr/local/lib/python2.7/dist-packages/tornado/concurrent.py", line 111, in result
raise self._exception
HTTPError: HTTP 400: Bad Request
----------------------------------------------------------------------
Ran 1 test in 0.022s
FAILED (errors=1)
[E 140929 12:55:59 testing:687] FAIL
Obviously this is correct, because the handler sets 400 status code. But how can I test my application for such case? I think 4xx codes are useful, so I don't want to give them up. However I'm new to Tornado and I wasn't able to find a way to test them. Is there any?

Try this:
#gen_test
def test_bad_request(self):
request = HTTPRequest(url=self.get_url('/'))
with self.assertRaises(tornado.httpclient.HTTPError) as context:
yield self.http_client.fetch(request)
self.assertEqual(context.exception.code, 400)
See the documentation for assertRaises.

Related

Flask app_context() used in threaded class but query doesn't work

I want to create a threaded class where I can do a polymorphic query to my DB to get the current subscriptions to an alarm notification. However the tests don't work when using pytest (but they work when using built in testing from VS Code) and in the dev environment there are some errors as well.
I have gone through quite a few answers on SO but most of the time people were not putting their app in the arguments of the threaded class. In some answers ._get_current_object() is appended to the app but this results in another error (see below).
I've been at this for a few days so if you have any suggestions, please let me know...
Threaded class:
class SubscriptionMapper(Thread):
instance = None
def __init__(self, app):
super().__init__()
self._app = app
try:
self.alarms = self._read_config()
...
def _read_config(self):
with self._app.app_context():
subscriptions = with_polymorphic(Subscription, "*").query.all()
print(subscriptions)
...
#staticmethod
def get_instance(app):
if SubscriptionMapper.instance is None:
SubscriptionMapper.instance = SubscriptionMapper(app)
return SubscriptionMapper.instance
#staticmethod
def get_alarm_description(app, alarm_uuid):
m = SubscriptionMapper.get_instance(app)
alarm_uuid = str(alarm_uuid)
return m.alarms.get(alarm_uuid, None)
Error in dev environment:
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/local/lib/python3.9/threading.py", line 954, in _bootstrap_inner
self.run()
File "/flask/app/notification/subscriptions.py", line 103, in run
self._read_config()
File "/flask/app/notification/subscriptions.py", line 119, in _read_config
with self._app.app_context():
File "/flask/venv/lib/python3.9/site-packages/werkzeug/local.py", line 347, in __getattr__
return getattr(self._get_current_object(), name)
File "/flask/venv/lib/python3.9/site-packages/werkzeug/local.py", line 306, in _get_current_object
return self.__local()
File "/flask/venv/lib/python3.9/site-packages/flask/globals.py", line 52, in _find_app
raise RuntimeError(_app_ctx_err_msg)
RuntimeError: Working outside of application context.
This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context(). See the
documentation for more information.
One of the tests which isn't working properly:
#pytest.fixture(scope='function')
def setup_subscription(db):
am = Alarm(
)
sub = Subscription(
alarm=am
)
db.session.add_all((am, sub))
db.session.commit()
yield am
def test_get_alarm_description(self, app, setup_subscription):
am = setup_subscription
assert SubscriptionMapper.get_alarm_description(
app, f'{am.uuid}') == 'Testproject - MON-1 - Verplaatsing in x (± 20.0 mm)'
Conftest.py
#pytest.fixture(scope='function')
def app() -> Flask:
app = create_app(config_name='unittest')
with app.app_context():
yield app
#pytest.fixture(scope='function')
def db(app: pytest.fixture) -> database:
flask_migrate.upgrade()
yield database
database.session.remove()
database.drop_all()
database.engine.execute(
'DROP TABLE IF EXISTS alembic_version;'
)
When executing the test by running pytest tests/test_notification_subscriptions.py the test fails with the following error
self = <tests.test_notification_subscriptions.TestSubscriptionMapper object at 0x0000024819B83FA0>, app = <Flask 'app'>, setup_subscription = <Alarm 6dcc5b00-9037-4c98-beae-91319423c7d6>
def test_get_alarm_description(self, app, setup_subscription):
am = setup_subscription
> assert SubscriptionMapper.get_alarm_description(
app, f'{am.uuid}') == 'Testproject - MON-1 - Verplaatsing in x (± 20.0 mm)'
E AssertionError: assert None == 'Testproject - MON-1 - Verplaatsing in x (± 20.0 mm)'
However when running the test from the built in testing tool in VS Code the test succeeds but prints an error in the Python Test Log output screen after succeeding.
======================= 1 passed, 35 warnings in 13.14s =======================
Exception in thread Thread-1:
Traceback (most recent call last):
File "C:\Users\Jan-P\miniconda3\envs\MeMo_AlarmService\lib\site-packages\sqlalchemy\engine\base.py", line 1276, in _execute_context
self.dialect.do_execute(
File "C:\Users\Jan-P\miniconda3\envs\MeMo_AlarmService\lib\site-packages\sqlalchemy\engine\default.py", line 608, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: no such table: tbl_subscription
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\Jan-P\miniconda3\envs\MeMo_AlarmService\lib\threading.py", line 954, in _bootstrap_inner
self.run()
File "c:\Users\Jan-P\Repos\MEMO\alarmservice\app\notification\subscriptions.py", line 106, in run
self._read_config()
File "c:\Users\Jan-P\Repos\MEMO\alarmservice\app\notification\subscriptions.py", line 123, in _read_config
subscriptions = with_polymorphic(Subscription, "*").query.all()
...
When adding ._get_current_object() to the get_instance function both types of tests result in the following error AttributeError: 'Flask' object has no attribute '_get_current_object'
#staticmethod
def get_instance(app):
if SubscriptionMapper.instance is None:
SubscriptionMapper.instance = SubscriptionMapper(app._get_current_object())
return SubscriptionMapper.instance

How to use application context to mock flask request

I am trying to properly unit test my Flask 1.1.2 routes, but do not really understand the documentation around mocking the application context.
Here is my class Launch.py:
from flask import Flask, request
from LaunchHelper import runController
#Create app
app = Flask(__name__)
#app.route('/write-database',methods=['POST'])
def writeDatabase():
return runController(request)
Here is my new TestLaunch.py after following the comment of #Felipe Emirem:
import unittest
from mock import patch, MagicMock
from flask import request #added
from Launch import app #added
class TestLaunch(unittest.TestCase):
#patch('Launch.runController')
def test_writeDatabase(self,runController):
resp = MagicMock()
runController.return_value = resp
with app.test_client() as c:
ret = c.post('/write-database')
#assert ret == resp
runController.assert_called_with(request)
if __name__ == '__main__':
unittest.main()
I can now assert that runController was called with flask.request by importing the app directly from Launch.py, which makes sense. But I am not sure how to confirm that the response is returning correctly.
Additionally I am now getting the following error:
ERROR in app: Exception on /write-database [POST]
Traceback (most recent call last):
File "C:\ProgramData\Anaconda3\lib\site-packages\flask\app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "C:\ProgramData\Anaconda3\lib\site-packages\flask\app.py", line 1953, in full_dispatch_request
return self.finalize_request(rv)
File "C:\ProgramData\Anaconda3\lib\site-packages\flask\app.py", line 1968, in finalize_request
response = self.make_response(rv)
File "C:\ProgramData\Anaconda3\lib\site-packages\flask\app.py", line 2117, in make_response
rv = self.response_class.force_type(rv, request.environ)
File "C:\ProgramData\Anaconda3\lib\site-packages\werkzeug\wrappers\base_response.py", line 269, in force_type
response = BaseResponse(*_run_wsgi_app(response, environ))
File "C:\ProgramData\Anaconda3\lib\site-packages\werkzeug\wrappers\base_response.py", line 26, in _run_wsgi_app
return app_iter, response[0], Headers(response[1])
IndexError: list index out of range
Even though the test is passing.
*Edit: Here is my old test class TestLaunch.py before I changed to use app.test_context():
import unittest
from mock import patch, MagicMock
from Launch import writeDatabase
class TestLaunch(unittest.TestCase):
#patch('Launch.runController')
#patch('Launch.request')
def test_writeDatabase(self,request,runController):
resp = MagicMock()
runController.return_value = resp
ret = writeDatabase()
assert ret == resp
runController.assert_called_with(request)
if __name__ == '__main__':
unittest.main()
I ran this on older versions of flask, and it worked fine, but now I am getting Working out of request context error due to the #patch('Launch.request') line. I have tried to read through other stackoverflow posts and the flask documentation, but I can't really find anything that applies to my current use case.
Ok, I figured it out. Just needed to use the test_request_context which I read about here: link.
Here is my new TestLaunch.py:
import unittest
from mock import patch, MagicMock
from flask import request
from Launch import app, writeDatabase
class TestLaunch(unittest.TestCase):
#patch('Launch.runController')
def test_writeDatabase(self,runController):
resp = MagicMock()
runController.return_value = resp
with app.test_request_context() as c:
ret = writeDatabase()
assert ret == resp
runController.assert_called_with(request)
if __name__ == '__main__':
unittest.main()
I am not testing the URL (which I wasn't originally anyway), but I am able to test that the flask request object is being handled properly.

How to Mock/Patch App Engine's Oauth Decorator?

App Engine's python client library has made oauth flow really easy with the following decorator.
#decorator.oauth_required
But it's really not straightforward to mock/patch for unit testing. For example in the following get handler, I need to stub out oauth decorator.
from auth import decorator
class ListUsersHandler(webapp2.RequestHandler):
#decorator.oauth_required
def get(self):
self.response.write(_RenderUserListTemplate())
I have tried something like below.
from mock import patch
patch('decorator.oauth_required', lambda x: x).start()
import user
class MyTest(unittest.TestCase):
def setUp(self):
app = webapp2.WSGIApplication([('/', user.ListUsersHandler)])
self.testapp = webtest.TestApp(app)
def testListUsersHandler(self):
response = self.testapp.get('/')
self.assertTrue(('list tokens' in response))
But, what I'm seeing this error, which doesn't seem to give much clue.
Traceback (most recent call last):
File "user_test.py", line 44, in testAbc
response = self.testapp.get('/')
File "/usr/local/lib/python2.7/dist-packages/webtest/app.py", line 322, in get
expect_errors=expect_errors)
File "/usr/local/lib/python2.7/dist-packages/webtest/app.py", line 605, in do_request
res = req.get_response(app, catch_exc_info=True)
File "/google_appengine/lib/webob-1.2.3/webob/request.py", line 1292, in send
application, catch_exc_info=True)
File "/google_appengine/lib/webob-1.2.3/webob/request.py", line 1269, in call_application
return (captured[0], captured[1], app_iter, captured[2])
IndexError: list index out of range

tornadoredis raise ConnectionError when disconnecting

I'm using tornado and tornado-redis but I can't close the connection to redis without getting an error. Look at this example:
import tornadoredis
import tornado.web
import tornado.gen
client = tornadoredis.Client()
client.connect()
class MainHandler(tornado.web.RequestHandler):
#tornado.web.asynchronous
#tornado.gen.engine
def get(self):
client.publish('test_channel', 'ahahah')
self.finish('Ok')
class ListenerHandler(tornado.web.RequestHandler):
#tornado.web.asynchronous
#tornado.gen.engine
def get(self):
self.client = tornadoredis.Client()
self.client.connect()
yield tornado.gen.Task(self.client.subscribe, 'test_channel')
self.client.listen(self.from_redis)
def from_redis(self, msg):
print msg.kind
if msg.kind == 'message':
self.write(str(msg.body))
self.aaaa()
if msg.kind == 'disconnect':
self.write('Redis error')
self.aaaa()
def aaaa(self):
print('aaaa')
self.finish()
if self.client.subscribed:
self.client.unsubscribe('test_channel')
self.client.disconnect()
def main():
app = tornado.web.Application([
(r"/", MainHandler),
(r"/listen", ListenerHandler),
])
app.listen(9898)
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
and test from command line with:
curl "http://yourip:9898/listen" &
curl "http://yourip:9898/"
All work fine except that the following error is returned when "self.client.disconnect()" is called:
Traceback (most recent call last):
File "/usr/local/lib/python2.7/dist-packages/tornado/web.py", line 1115, in _stack_context_handle_exception
raise_exc_info((type, value, traceback))
File "/usr/local/lib/python2.7/dist-packages/tornado/stack_context.py", line 302, in wrapped
ret = fn(*args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 550, in inner
self.set_result(key, result)
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 476, in set_result
self.run()
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 505, in run
yielded = self.gen.throw(*exc_info)
File "/usr/local/lib/python2.7/dist-packages/tornadoredis/client.py", line 1070, in listen
data = yield gen.Task(self.connection.readline)
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 533, in run
self.yield_point.start(self)
File "/usr/local/lib/python2.7/dist-packages/tornado/gen.py", line 371, in start
self.func(*self.args, **self.kwargs)
File "/usr/local/lib/python2.7/dist-packages/tornadoredis/connection.py", line 154, in
readline
raise ConnectionError('Tried to read from '
ConnectionError: Tried to read from non-existent connection
Do you have any suggestion to get the connection to redis closed in a clean and right way ?

Flask-Sijax callbacks are "working outside of request context"

While inside a callback function, I lose the ability to access flask.session, flask.g, or functions such as url_for(). They all throw an error saying that I'm "working outside of request context".
Debugging middleware caught exception in streamed response at a point where response headers were already sent.
Traceback (most recent call last):
File "C:\Python27\site-packages\sijax\response\streaming.py", line 136, in _process_call_chain
for string in generator:
File "C:\Python27\site-packages\sijax\response\streaming.py", line 109, in _process_callback
response = self._perform_handler_call(callback, args)
File "C:\Python27\site-packages\sijax\response\base.py", line 258, in _perform_handler_call
return callback(self, *args)
File "C:\Dropbox\Code\Python 2.7\FlaskTesting\testpage.py", line 18, in myformhandler
sql_session = flask.g.sql_session
File "C:\Python27\lib\site-packages\werkzeug\local.py", line 336, in __getattr__
return getattr(self._get_current_object(), name)
File "C:\Python27\lib\site-packages\werkzeug\local.py", line 295, in _get_current_object
return self.__local()
File "C:\Python27\lib\site-packages\flask\globals.py", line 19, in _lookup_object
raise RuntimeError('working outside of request context')
RuntimeError: working outside of request context
192.168.1.141 - - [20/Jun/2012 16:33:04] "POST /testpage HTTP/1.1" 200 -
I've been unable to find out how to get around this problem. Any help would be appreciated.
Python v2.7
Flask v0.8
Flask-Sijax v0.3
You may have a try with stream_with_context. The code example copied from http://flask.pocoo.org/docs/0.12/patterns/streaming/#streaming-with-context
from flask import stream_with_context, request, Response
#app.route('/stream')
def streamed_response():
def generate():
yield 'Hello '
yield request.args['name']
yield '!'
return Response(stream_with_context(generate()))
It would be helpful if you posted your code, but try wrapping your code like this:
with app.app_context():
# do stuff...
or maybe this:
with app.test_request_context():
# do stuff...

Categories