Search code examples
unit-testingpython-3.xflaskcontextmanagerflask-testing

Why isn't my Flask application's global object deleted on app teardown?


I am writing an application with a global object that should live as long as the application as alive. Different endpoints should mutate the global object.

Below is my server with an example mock-up object to be mutated on dummy endpoint calls.

server.py

#!/usr/bin/env python3

from flask import Flask, g, request

class Foo(object):
    def __init__(self):
        self.bar = None

    def add_bar(self, bar):
        if self.bar is not None:
            raise Exception("You blew it!")
        self.bar = bar

def create_app():
    app = Flask(__name__)
    return app

app = create_app()

def get_foo():
    foo = getattr(g, '_foo', None)
    if foo is None:
        print("foo is None. Creating a foo")
        foo = g._foo = Foo()
    return foo

@app.teardown_appcontext
def teardown_foo(exception):
    foo = getattr(g, '_foo', None)
    if foo is not None:
        print("Deleting foo")
        del foo

@app.route("/add_bar", methods=['POST'])
def bar():
    bar = request.form.get("bar")
    foo.add_bar(bar)
    return "Success"    

if __name__ == "__main__":
    app = create_app()
    app.run('localhost', port=8080)

Like a good software developer, I want to test this. Here's my test suite:

test.py

#!/usr/bin/env python3

from server import *
import unittest

class FlaskTestCase(unittest.TestCase):
    def setUp(self):
        app.config['TESTING'] = True
        print("Creating an app...")
        self.app = create_app()
        print("Created an app...")
        with self.app.app_context():
            self.foo = get_foo()

    def tearDown(self):
        del self.foo

    def test_add_bar(self):
        with self.app.test_client() as client:
            client.post("/add_bar", data={'bar': "12345"})
            assert self.foo.bar == "12345"

if __name__ == "__main__":
    unittest.main()

When I run the test, I notice that my foo object is never deleted (by the prints). Here's the output when I run the test suite:

13:35 $ ./test.py
Creating an app...
Created an app...
foo is None. Creating a foo
F
======================================================================
FAIL: test_add_bar (__main__.FlaskTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./test.py", line 21, in test_add_bar
    assert self.foo.bar == "12345"
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.009s

FAILED (failures=1)

I must be doing something incorrectly with my global object with respect to the application context. I've pored over the testing documents, but cannot seem to find exactly what I need.

How can I ensure my foo object is destroyed when the application context goes out of scope (like in my tests)? Why isn't my unit test suite mutating its foo object that is created on setUp?

I thought it could be modifying the global app object within server.py, but when I printed foo, I found that it wasn't defined within the scope of my test suite.


Solution

  • This was a weird issue.

    The problem was an interference between the global app object within the server.py and the app being created within the original test.py. Specifically, since the app was being imported, its lifetime was the life of the test suite.

    I removed the global app object by using flask.Blueprint. Here's the final server.py:

    #!/usr/bin/env python3
    
    from flask import Flask, g, request, Blueprint
    
    main = Blueprint('main', __name__)
    
    def get_foo():
        foo = getattr(g, 'foo', None)
        if foo is None:
            foo = g.foo = Foo()
        return foo
    
    @main.before_request
    def before_request():
        foo = get_foo()
    
    @main.route("/add_bar", methods=['POST'])
    def bar():
        bar = request.form.get("bar")
        g.foo.add_bar(bar)
        return "Success"
    
    class Foo(object):
        def __init__(self):
            self.bar = None
    
        def add_bar(self, bar):
            if self.bar is not None:
                raise Exception("You blew it!")
            self.bar = bar
    
    def create_app():
        app = Flask(__name__)
        app.register_blueprint(main)
        @app.teardown_appcontext
        def teardown_foo(exception):
            if g.foo is not None:
                del g.foo
        return app
    
    if __name__ == "__main__":
        app = create_app()
        with app.app_context():
            foo = get_foo()
            app.run('localhost', port=8080)
    

    And here's the final test.py:

    #!/usr/bin/env python3
    
    from server import create_app, get_foo
    import unittest
    
    class FlaskTestCase(unittest.TestCase):
        def setUp(self):
            self.app = create_app()
            self.app_context = self.app.app_context()
            self.app_context.push()
            self.client = self.app.test_client()
            self.foo = get_foo()
    
        def tearDown(self):
            self.app_context.pop()
            del self.foo
    
        def test_add_bar_success(self):
            assert self.foo.bar is None
            self.client.post("/add_bar", data={'bar': "12345"})
            assert self.foo.bar == "12345"
    
        def test_foo_reset_on_new_test(self):
            assert self.foo.bar is None
    
    if __name__ == "__main__":
        unittest.main()
    

    test_foo_reset_on_new_test illustrates that the foo object associated with the test suite is reset on each test.

    All tests pass.