Have you ever tried to test POST requests to an API written with Flask? Today I was going through an old code base writing unittests that should have been written eons ago when I ran into a snag. The issue stemmed when I tried manually setting the headers Content-Type.
But before we get to that, let me show you the skeleton of the Flask route I was testing:
@app.route('/someendpoint/' methods=['POST']) def some_endpoint(): """API endpoint for submitting data to :return: status code 405 - invalid JSON or invalid request type :return: status code 400 - unsupported Content-Type or invalid publisher :return: status code 201 - successful submission """ # Ensure post's Content-Type is supported if request.headers['content-type'] == 'application/json': # Ensure data is a valid JSON try: user_submission = json.loads(request.data) except ValueError: return Response(status=405) ... some magic stuff happens if everything_went_well: return Response(status=201) else: return Response(status=405) # User submitted an unsupported Content-Type else: return Response(status=400)
Ignoring the magic, everything seems in order. So, lets go ahead and write a quick unittest that posts an invalid json.
def test_invalid_JSON(self): """Test status code 405 from improper JSON on post to raw""" response = self.app.post('/someendpoint', data="This isn't a json... it's a string!") self.assertEqual(response.status_code, 405)
Cool, let’s run it!
Failure self.assertEqual(response.status_code, 405) AssertionError: 400 != 405
A quick glance at the code and I realize I’ve forgotten to set the headers Content-Type! Wait. How do I set the Content-Type? That’s a good question. After searching around for people who had run into similar problems this is what I came up with:
def test_invalid_JSON(self): """Test status code 405 from improper JSON on post to raw""" response = self.app.post('/someendpoint', data="This isn't a json... it's a string!", headers={'content-type':'application/json') self.assertEqual(response.status_code, 405)
Let’s try this again.
Failure self.assertEqual(response.status_code, 405) AssertionError: 400 != 405
Hmmm, okay. Next, I decided to inspect the request coming into the flask app and found something odd in request.headers:
Host: localhost Content-Length: 10 Content-Type:
Why is the Content-Type empty? Another search gave hinted at another possible solution. Why not just build the headers dict inline?
headers=dict(content-type='application/json') # But that's not right. We can't have '-' in a key.
By this point I’ve become agitated. Neither the Flask docs themselves nor various forums have been of much use. Then I stumbled across this.
Perhaps I missed something in the docs. Either way, I learned that you can hand the Content-Type as a parameter to the post method. It works and it looks much cleaner. Let’s revise my initial unittest accordinlgy:
def test_invalid_JSON(self): """Test status code 405 from improper JSON on post to raw""" response = self.app.post('/raw', data="not a json", content_type='application/json') self.assertEqual(response.status_code, 405)
And, let’s run this one last time and look at the request as it comes though.
Tests passed Host: localhost Content-Length: 10 Content-Type: application/json
Much better. Now, back to testing!
-H.
This would be even easier with webtest: http://webtest.readthedocs.org/en/latest/. It provides a post_json method that includes the appropriate headers and dumps data to JSON. Also check out Flask’s request.json property, which saves you the effort of checking headers, parsing JSON, raising appropriate exceptions, and probably other stuff.
Nice, Josh! I knew I should have pinged you when I ran into issues…