Developing (a.k.a. Testing) in Python - Part 2
Join the DZone community and get the full member experience.
Join For FreeIn part one I talked about testing as Python’s version of compile-time checking. Fully
testing a project is tricky business. This is true across languages;
even advanced testing frameworks can only alleviate some problems. One
particular pain point is testing components that strongly rely on
databases. To run test code involving databases, we need
scripts/functions that recreate our testing environment and put in basic
data to get things running. Suppose we’re using MySQL and we want to
start our database fresh for each test. We can run a fixtures script
before each test that might look something like:
DROP DATABASE fiesta_dev; CREATE DATABASE fiesta_dev; CREATE TABLE `users` (^C ...wait what is the syntax again? USE fiesta; SHOW CREATE TABLE users; CREATE ^C USE fiesta_dev; CREATE TABLE `users` (`id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64), `password` CHAR(32), `email_address` VARCHAR(128)); User.new_user('Daniel Gottlieb', 'some password hash', 'dan@corp.fiesta.cc')
And things are good. Until one day we realize that our user data
model needs to support linking multiple email addresses. Oops. We add a
new `email_addresses` table to our database. It has an index on
`user_id` and on `email_address`. There’s a foreign key constraint on
`user_id` to the `users` table. We now update our production code to
work with a list of strings for email addresses instead of a single one.
And we’re done. Except that now our tests don’t work. We have to add in
a new “create table” to our script. We also have to update the `users`
table in the test script. This leaves us with:
... CREATE TABLE `users` (`id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64), `password` CHAR(32)); CREATE TABLE `email_addresses` (`user_id` INT, `email_address` VARCHAR(128), INDEX (`email_address`), CONSTRAINT FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE); user_obj = User.new_user('Daniel Gottlieb', 'some password hash', 'dan@corp.fiesta.cc') user_obj.add_email_address('dgottlieb@corp.fiesta.cc')
Let’s look at our fixture script if we use MongoDB (and pymongo) instead:
conn = pymongo.Connection() conn.drop_database("fiesta_dev") user_obj = User.new_user('Daniel Gottlieb', 'some password hash', 'dan@corp.fiesta.cc')
Now let’s add support for lists of emails and see how our fixtures script changes:
conn = pymongo.Connection() conn.drop_database("fiesta_dev") user_obj = User.new_user('Daniel Gottlieb', 'some password hash', 'dan@corp.fiesta.cc') user_obj.add_email_address('dgottlieb@corp.fiesta.cc')
Admittedly, there is a little bit of “magic” here; particularly with generating indexes. The way Fiesta
is setup, important sections of code will redundantly call create_index
on a table. If the index is already created, MongoDB ignores the
statement. This is similar to how MongoDB will silently create a new
collection when its first document is inserted. Combined with the
ability to add any field to any document in a collection, the fixtures
script only needs to be aware of the database enough to clear all the
data before the tests run.
I hope this offers some insight into why I think cleaner test code can be written with MongoDB than with a relational database. Feel free to ask questions or offer up your own tips in the comments below!
Party On,
Dan
source: http://blog.fiesta.cc/post/11668794861/developing-a-k-a-testing-in-python-part-two
Opinions expressed by DZone contributors are their own.
Comments