A Flask Mega-Tutorial: Part III

Welcome back to my mega-tutorial on Flask. If you’re following along with Part I and Part II, you should already have a minimal Flask application that doesn’t really do anything meaningful. While I had said before, this part would be all about testing, you may think “Why? It doesn’t do anything!”. That’s correct.

It’s time to appreciate the requirements on the application. You may or may not have an extensive comprehension of some of the things it may need to do — I did when I got started, but what precisely the application needed to do was not yet clear.

What the application is going to need to be able to do, and how the application will go about achieving exactly that is subject to specification. Clear specification of the requirements, as well as clear specification of how to achieve those targets.

This part revolves around testing, but I know I’m going to need to be more specific — it is about unit testing, and arguably also functional testing, with help of fixtures.

We’ll be using Python’s unittest in combination with Flask-Fixtures. We’ll also use the fixtures to provide your application with some content, so it is not as dull outside of testing — the tests first wipe the database, then populate the database, but tear down and drop all tables from the database when they are done.

First things first, I propose you put your tests in the directory named, well, err… tests/. A simple naming convention to ensure tests are ordered (correctly), is to name the individual files in a pattern test_$x_topic.py, such as for example tests/test_000_schema.py.

This is the first give-away. Test your schema to ensure it function as you expect it to. This means when you expect an ON DELETE CASCADE to function such that the associated items are removed when you remove the referred item, you test this.

So let’s start a database model. Again, I cannot emphasize enough how important it is to get your requirements sorted. In our example, we’re going to have items and one owner per item.

The following is an example of ppppp/db/__init__.py. Note that I would normally put different classes in to different files inside of a model/ subdirectory, … useful especially when database models grow larger.

This enables us to import the database and model in ./manage.py so let’s do that:

If you were to run ./manage.py now, you’ll find tracebacks about the database not yet having been created, which is correct:

$ python -c 'from ppppp.db import db; db.create_all()'

This should resolve that problem. However your database is empty. This is where fixtures come in.

Create tests/fixtures/owners.json, and put in it:

Create tests/fixtures/items.json, and put in it:

Now, we’re going to switch over ./manage.py to use the Flask-Script extension:

And we can now run: ./manage.py load_fixtures. This will drop the database tables and re-create them, and add our fixtures.

You should now also use ./manage.py runserver to run the actual webserver, and when you do, you should see:

{
  "Clock": "Jane", 
  "Watch": "John"
}

Now we can start testing.

Again, a nice little naming convention to ensure tests are executed in order may be to name them test_$x_$subject, as follows:

Let’s read back as to what we’re verifying;

In test_001_owners(), we verify the tests/fixtures/owners.json file was loaded successfully, and completely. Boring, but necessary.

In test_002_items(), we verify the tests/fixtures/items.json file was loaded successfully and completely. Boring, but even more necessary than the test for tests/fixtures/owners.json, because of the foreign key restrictions.

In test_003_owner_delete_cascade() however, we test the real deal: Is the item deleted when the user is? It is important to realize that just writing this one test would fail to pass the mark, because an owner Jane could simply have no items, because the schema, fixtures or even the tests could have been (written) wrong.

Running the tests would result in:

$ venv/bin/nosetests -v tests
test_001_owners (test_000_schema.TestSchema) ... ok
test_002_items (test_000_schema.TestSchema) ... ok
test_003_owner_delete_cascade (test_000_schema.TestSchema) ... ok

----------------------------------------------------------------------
Ran 3 tests in 2.018s

OK

Hence verifying we have specified the ON DELETE CASCADE part of the schema correctly.

At this point, I would suggest inserting more tests to create a duplicate entry John or Jane, set a unique constraint for Owner.name, etc.

However, we cannot test the web application in full. Adding a test such as:

Will fail with a 404 Not Found error. Why? Because the app we are using does not use the routes we have in ./manage.py.

Here’s where that ppppp/web/ directory comes in. In effect, ./manage.py only needs a very minimal configuration of the application, so that it can load fixtures and do other such stuff, but more importantly, run what is otherwise app. It could import that from some location so that other stuff can also import it from that location. We’re going to be using ppppp/web/webapp.py for this:

And we make it so it can be imported sanely (i.e. without having to expose everything), through ppppp/web/__init__.py:

Now we can dumb down ./manage.py quite a bit:

The tests we can now write include interaction with the application, without having to copy routes and other such application logic across all tests.

Advertisements