0

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.

Community
  • 1
  • 1
erip
  • 16,374
  • 11
  • 66
  • 121
  • 1
    `except: assert False` looks like an antipattern to me. Probably the best place to start is to remove the try/except and just let the exception bubble up. Looking at the original exception may be illuminating. (On a side note, `assert True` will always be completely useless. It seems like maybe you have some misunderstandings about how assertions should work.) – jpmc26 Aug 11 '16 at 17:33
  • 1
    @jpmc26 I don't think I have misunderstandings about how assertions work... I copied the test from something a bit more complicated and boiled it down. Didn't remove extraneous stuff. Will update. – erip Aug 11 '16 at 17:35

2 Answers2

0

Because del foo only unbind the variable foo. If you have foo = g._foo, then calling del foo would not unbind g._foo, but only foo itself.

del is not used to delete an object. It releases a variable name from scope.

Quan To
  • 697
  • 3
  • 10
  • 1
    1. `teardown_foo` is never called. 2. `foo` and `g._foo` are the same object. 3. Unbinding a name to an object and reusing that name is the same as "deleting" an object. – erip Aug 12 '16 at 13:00
  • 1
    @erip unbinding the name `foo` doesn't unbind the name `g._foo`. If the object is still referenced somewhere, it does not get destroyed. – Quan To Aug 15 '16 at 11:09
0

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.

erip
  • 16,374
  • 11
  • 66
  • 121