Flask-Twitter-OEmbedder and Flask Extension Testing

  • Flask
  • Python
  • Testing

Flask-Twitter-OEmbedder

The Twitter API V1.1 provides an endpoint for easily embedding tweets into your webpages. However, Twitter rate limits this endpoint and requires authorization. Flask Twitter OEmbedder gets your Twitter credentials from your app.config and exposes the oembed_tweet function to the Jinja templates. Flask Twitter OEmbedder also manages the caching of the tweets, given an arbitrary cache using Flask-Cache.

You can check it out and fork it on GitHub. Feature requests, issues, and anything else are encouraged! This is very much alpha and I plan to continue working on it.

Flask Extension Testing

Flask-Twitter-OEmbedder was the first Flask extension I have written (and really the first proper Python package I've ever written). This summer I am lucky enough to be participating in Hacker School and one of my goals for the batch was to be able to write unit tests for my code that I develop. Since this was my first proper package, I figured it was a great place to start.

Testing in the abstract seems reflexive and redundent; I wrote code specifically to perform x, why would I write more code to make sure that it does x? This was a particularly difficult mental block for me, especially coming to development from mathematics. However, as your code base grows and relies on more and more abstraction, when something breaks, testing helps you find exactly what you broke and where to fix it. It's not uncommon for libraries and extensions to change their API's at times, in which case it may not even be code that you wrote to do x, and now it does y. Luckily earlier this year, I attended PyCon and saw this great presentation on getting starting with automated testing Python.

After getting past my mental testing block, I was faced with a few other challanges with getting testing working with my Flask extension. I had broken my extension into a handful of base states which I wanted to test:

  • the oembed_tweet() function is avaliable in the Jinja templates
  • the oembed_tweet() function properly embeds a tweet when given a valid tweet id
  • the oembed_tweet() function fails properly (depending on the app.debug and local debug state) when given an invalid tweet id

Each of these presented their own challanges. The first challange however, was just approaching the problem at hand. The testing documentation for Flask is all based for testing Flask apps, not Flask extensions. However, it quickly became clear that it was sufficent to use Flask-Testing to create a "dummy" app which would utilize my extension and be able to test if it worked properly. Unfortunetly, Flask-Testing doesn't have a method like assertExists, so I ended up testing

 assert type(self.get_context_variable('oembed_tweet')) is types.FunctionType

The next challange was being able to run the tests offline. In general, unit tests shouldn't care if external API's are working or not. It is certainly useful and appropriate in some cases to write tests that to check for this, but for this test we just want to know if our code is working as expected, assuming that Twitter's API is working correctly.We(I paired with one of the awesome Hacker School facilitators, Zach Allaun) started with vcrpy, which is a port of the Ruby vcr gem, however this didn't work quite right. We then moved onto cassette but it unfortunetly didn't play nice with HTTPS. Finally we landed on HTTPretty which did exactly what we needed. We grabbed the actual response from Twitter and dumped it into a JSON, then HTTPretty blocks any out going requests to specific urls and returns the content of the JSON as if the request had gone through normally. It can be used as followed:

@httpretty.activate
def test_oembed_tweet_valid_id_debug_off(self):
    with open('tests/data/99530515043983360.json') as f:
        httpretty.register_uri(httpretty.GET, 'https://api.twitter.com/1.1/statuses/oembed.json?id=99530515043983360',
            body = f.read())
    response = self.client.get('/')
    oembed_tweet = self.get_context_variable('oembed_tweet')
    valid = oembed_tweet('99530515043983360')
    assert type(valid) is Markup

In this example, the oembed_tweet fuction makes a GET request using the requests package to the uri that we specified in the httpretty.register_uri() call, and the response is what we specified as the body in the same function.

One final interesting case is when we wanted to test what happens when we want to test how Flask-Twitter-OEmbedder handles an error from the Twitter API. In a development setting, we likely want this to actually raise an exception so that we can investigate what is causing the error (this could be a number of things: incorrect tweet id, Twitter API outage, deleted tweet, etc), but in production we would likely want it to fail gracefully and just return an empty string. Testing for an exception is fairly straight forward:

try:
    invalid = oembed_tweet('abc')
except Exception as e:
    assert type(e) is KeyError

There is one little trick here, however, that is easy to overlook. We are expecting the second line to fail, and then we are asserting that the failure is the type that we expect. However, what if the second line doesn't fail? In the context of the test, we would classify this as a failure, since the failure on line 2 is what we want. But in the code above, if line 2 doesn't fail for some reason, the assert will never happen and the test will pass. We can fix this by adding one line after line 2:

try:
    invalid = oembed_tweet('abc')
    assert False
except Exception as e:
    assert type(e) is KeyError

The addition of assert False will force the try clause to fail, even if line 2 doesn't fail as expected, and since assert False raises an Assertion Error, line 5 will fail properly.