I am trying to locally test a Python function that I hope to deploy as a Google Cloud Function. These functions seem to be essentially Flask based, and I have found that the best way to return JSON is to use Flask's jsonify function. This seems to work fine when deployed, but I want to set up some local unit tests, and here is where I got stuck. Simply adding the line to import jsonify, results in the following error:
RuntimeError: Working outside of application context.
There are several posts here on Stackoverflow that seem relevant to this issue, and yet Google Cloud Functions do not really follow the Flask pattern. There is no app context, as far as I can tell, and there are no decorators. All of the examples I've found have not been useful to this particular use case. Can anyone suggest a method for constructing a unit test that will respect the application context and still jibe with the GCF pattern here.
I have a unittest, which I can share, but you will see the same error when you run the following, with the method invocation inside of main.
import os
import json
from flask import jsonify
from unittest.mock import Mock
def dummy_request(request):
request_json = request.get_json()
if request_json and 'document' in request_json:
document = request_json['document']
else:
raise ValueError("JSON is invalid, or missing a 'docuemnt' property")
data = document
return jsonify(data)
if __name__ == '__main__':
data = {"document":"This is a test document"}
request = Mock(get_json=Mock(return_value=data), args=data)
result = dummy_request(request)
print(result)
You don't really need to test whether flask.jsonify works as expected, right? It's a third-party function.
What you're actually trying to test is that flask.jsonify was called with the right data, so instead you can just patch flask.jsonify, and make assertions on whether the mock was called:
import flask
from unittest.mock import Mock, patch
def dummy_request(request):
request_json = request.get_json()
if request_json and 'document' in request_json:
document = request_json['document']
else:
raise ValueError("JSON is invalid, or missing a 'docuemnt' property")
data = document
return flask.jsonify(data)
#patch('flask.jsonify')
def test(mock_jsonify):
data = {"document": "This is a test document"}
request = Mock(get_json=Mock(return_value=data), args=data)
dummy_request(request)
mock_jsonify.assert_called_once_with("This is a test document")
if __name__ == '__main__':
test()
I'd recommend you to take a look at Flask's documentation on how to test Flask apps, it's described pretty well how to setup a test and get an application context.
P.S. jsonify requires application context, but json.dumps is not. Maybe you can use the latter?
I came across the same issue. As you've said the flask testing doesn't seem to fit well with Cloud Functions and I was happy with how the code worked so didn't want to change that. Adding an application context in setUp() of testing then using it for the required calls worked for me. Something like this...
import unittest
import main
from flask import Flask
class TestSomething(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
def test_something(self):
with self.app.app_context():
(body, code) = main.request_something()
self.assertEqual(200, code, "The request did not return a successful response")
if __name__ == '__main__':
unittest.main()
Related
I'm getting started with Flask and Pytest in order to implemente a rest service with unit test, but i'm having some troouble.
I'll like to make a simple test for my simple endpoint but i keep getting a Working outside of application context. error when running the test.
This is the end point:
from flask import jsonify, request, Blueprint
STATUS_API = Blueprint('status_api', __name__)
def get_blueprint():
"""Return the blueprint for the main app module"""
return STATUS_API
#STATUS_API.route('/status', methods=['GET'])
def get_status():
return jsonify({
'status' : 'alive'
})
And this is how I'm trying to test it (i know it should fail the test):
import pytest
from routes import status_api
def test_get_status():
assert status_api.get_status() == ''
I'm guessing I just cant try the method with out building the whole app. But if that's the case i dont really know how to aproach this problem
The Flask documentation on testing is pretty good.
Instead of importing the view functions, you should create a so called test client, e.g. as a pytest fixture.
For my last Flask app this looked like:
#pytest.fixture
def client():
app = create_app()
app.config['TESTING'] = True
with app.app_context():
with app.test_client() as client:
yield client
(create_app is my app factory)
Then you can easily create tests as follows:
def test_status(client):
rv = client.get('/stats')
assert ...
As mentioned at the beginning, the official documentation is really good.
Have you considered trying an API client/development tool? Insomnia and Postman are popular ones. Using one may be able to resolve this for you.
I am running unit test to test an API that works fine when tested with Postman. The API takes in two parameters in the form {"body":"hey","title":"title"} adds these values to the database based on the models I have made. A response is returned in similar format with an extra key of id which is obtained from the database. The thing is that it works fine with Postman. However, when tested using the Pytest, just does not work.
Here is the code in the test file.
import os
import unittest
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
# from flask_backend import app
# from flask_backend.core import db
class BasicTests(unittest.TestCase):
app = Flask(__name__)
db = SQLAlchemy(app)
def setUp(self):
file_path = os.path.abspath(os.getcwd()) + "\database_test.db"
self.app.config['TESTING'] = True
self.app.config['DEBUG'] = False
self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + file_path
self.app = self.app.test_client()
self.db.drop_all()
self.db.create_all()
def tearDown(self):
self.db.session.remove()
class TestApi(BasicTests):
def test_add_post(self):
url = 'http://127.0.0.1:5000'
parameters = {'body': 'Body', 'title': 'title'}
response = self.app.post(url+'/api/dbapi/post/', data=parameters)
print(self.app)
self.assertEqual(response.status_code, 200)
if __name__ == '__main__':
unittest.main()
I am thinking that while executing the test a server is not started and hence the 404 error is raised.
The reason I am not importing the app variable from the project itself is because the module is not getting imported. I have asked the question in a different thread. Here is the link to it:
Can not import the files from parent directory even though it has __init__.py file in it
From what I understand, if I can import the app instance that is used in the project itself, I should be good but that isn't working either.
Here is how I solved the problem. So, apparently it was required to get the server running for the API to be accessible and the test to run successfully.
So, I added app.run() at the end of the code and it worked fine. Here is what it looks like
if __name__ == '__main__':
unittest.main()
app.run()
I'm getting started writing Google Cloud http Functions using Python and I would like to include unit tests. My function only responds to POST requests so the basic outline of my function is:
def entry_point(request):
if request.method == 'POST':
# do something
else:
# throw error or similar
I want to write a simple unit test to ensure that if the function receives a GET request the response has a 405 status.
Hence in my unit test I need to pass in a value for the request parameter:
def test_call(self):
req = #need a way of constructing a request
response = entry_point(req)
assert response.status == 405 # or something like this
Basically I need to construct a request then check that the response status is what I expect it to be. I've googled around and found loads of pages that talk about mocking and all sorts of stuff that I frankly don't understand (I'm not a crash hot developer) so I'm hoping someone can help me with an idiot's guide of doing what I need to do.
I did find this: https://cloud.google.com/functions/docs/bestpractices/testing#unit_tests_2:
from unittest.mock import Mock
import main
def test_print_name():
name = 'test'
data = {'name': name}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello {}!'.format(name)
def test_print_hello_world():
data = {}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello World!'
which kinda helped but it doesn't explain how I can specify the request method (i.e. GET, POST etc...).
It is probably late to also comment on this one, but I was going through the same quest. How to REALLY unit test a cloud function without mocks and/or Flask test client. For the records, at the Google Cloud Platform Python Docs Samples, it is explained how to use Flask test_request_context() functionality with some examples to achieve this (without having to create Request object by hand).
Just dawned on me that I was rather over-thinking this somewhat. For the purposes of testing my code doesn't actually require a real Flask request passed to it, it merely requires an object that has the attributes my code refers to. For me, this will do:
import unittest
from main import entry_point
class Request:
def __init__(self, method):
self.method = method
class MyTestClass(unittest.TestCase):
def test_call_with_request(self):
request = Request("GET")
response = entry_point(request)
assert response.status_code == 405
I have written a flask application that uses flask dance for user authentication. Now I want to test a few views for that I have enabled #login_required.
I wanted to follow the flask dance testing docs but I could not get it to work. Because I am only using unittest and not pytest. I also use github and not google as in the docs. So is sess['github_oauth_token'] correct? A prototype sample test could look like the following:
def test_sample(self):
with self.client as client:
with client.session_transaction() as sess:
sess['github_oauth_token'] = {
'access_token': 'fake access token',
'id_token': 'fake id token',
'token_type': 'Bearer',
'expires_in': '3600',
'expires_at': self.time + 3600
}
response = client.post(url_for('core.get_sample'), data=self.fake_sample)
self.assertRedirects(response, url_for('core.get_sample'))
The assertRedirect fails because I am redirected to the login page http://localhost/login/github?next=%2Fsample%2F and not url_for('core.get_sample').
Then tried to simply disable it, by following the official flask login docs.
It can be convenient to globally turn off authentication when unit
testing. To enable this, if the application configuration variable
LOGIN_DISABLED is set to True, this decorator will be ignored.
But this does not work as well, the test still fail because login_required is somehow executed.
So my questions are:
Because I am using github and not google as in the docs is github_oauth_token the correct key for the session?
How do I test views that have the #login_required decorator with unittest when using Flask Dance?
Edit: LOGIN_DISABLED=True works as long as I define it in my config class I use for app.config.from_object(config['testing']), what did not work was to set self.app.config['LOGIN_DISABLED'] = True in my setup method.
Even if you're using the unittest framework for testing instead of pytest, you can still use the mock storage classes documented in the Flask-Dance testing documentation. You'll just need to use some other mechanism to replace the real storage with the mock, instead of the monkeypatch fixture from Pytest. You can easily use the unittest.mock package instead, like this:
import unittest
from unittest.mock import patch
from flask_dance.consumer.storage import MemoryStorage
from my_app import create_app
class TestApp(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.client = self.app.test_client()
def test_sample(self):
github_bp = self.app.blueprints["github"]
storage = MemoryStorage({"access_token": "fake-token"})
with patch.object(github_bp, "storage", storage):
with self.client as client:
response = client.post(url_for('core.get_sample'), data=self.fake_sample)
self.assertRedirects(response, url_for('core.get_sample'))
This example uses the application factory pattern, but you could also import your app object from somewhere else and use it that way, if you want.
I'm creating a large number of Flask routes using regular expressions. I'd like to have a unit test that checks that the correct routes exist and that incorrect routes 404.
One way of doing this would be to spin up a local server and use urllib2.urlopen or the like. However, I'd like to be able to run this test on Travis, and I'm assuming that's not an option.
Is there another way for me to test routes on my application?
Use the Flask.test_client() object in your unittests. The method returns a FlaskClient instance (a werkzeug.test.TestClient subclass), making it trivial to test routes.
The result of a call to the TestClient is a Response object, to see if it as 200 or 404 response test the Response.status_code attribute:
with app.test_client() as c:
response = c.get('/some/path/that/exists')
self.assertEquals(response.status_code, 200)
or
with app.test_client() as c:
response = c.get('/some/path/that/doesnt/exist')
self.assertEquals(response.status_code, 404)
See the Testing Flask Applications chapter of the Flask documentation.
Martjin's answer surely solve your issue, but some times you don't have the time (or will) to mock all the components you call in a route you want to test for existence.
And why would you need to mock? Well, the call get('some_route') will first check for this route to exists and then ... it will be executed!
If the view is a complex one and you need to have fixtures, envs variables and any other preparation just for test if the route exists, then you need to think again about your test design.
How to avoid this:
You can list all the routes in your app. An check the one you're testing is in the list.
In the following example, you can see this in practice with the implementation of a site-map.
from flask import Flask, url_for
app = Flask(__name__)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
#app.route("/site-map")
def site_map():
links = []
for rule in app.url_map.iter_rules():
# Filter out rules we can't navigate to in a browser
# and rules that require parameters
if "GET" in rule.methods and has_no_empty_params(rule):
url = url_for(rule.endpoint, **(rule.defaults or {}))
links.append((url, rule.endpoint))
# links is now a list of url, endpoint tuples
references:
get a list of all routes defined in the app
Helper to list routes (like Rail's rake routes)
Another way of testing a URL without executing the attached view function is using the method match of MapAdapter.
from flask import Flask
app = Flask(__name__)
#app.route('/users')
def list_users():
return ''
#app.route('/users/<int:id>')
def get_user(id):
return ''
Testing
# Get a new MapAdapter instance. For testing purpose, an empty string is fine
# for the server name.
adapter = app.url_map.bind('')
# This raise werkzeug.exceptions.NotFound.
adapter.match('/unknown')
# This raises werkzeug.exceptions.MethodNotAllowed,
# Although the route exists, the POST method was not defined.
adapter.match('/users', method='POST')
# No exception occurs when there is a match..
adapter.match('/users')
adapter.match('/users/1')
From Werkzeug documentation:
you get a tuple in the form (endpoint, arguments) if there is a match.
Which may be useful in certain testing scenarios.