Python Web Framework Series – Pylons: Part 5 Testing Models

UPDATE: was an error caught by Govind (who is turning out to be my unofficial proofreader). I’ve made a correction in the thread mapping for “dateadded” property in the previous article. If this you are caught up with and error indicating there is no dateadded property on thread make sure to edit your model__init__.py file to match the previous article and rebuild your db so that everything is happy. Please bear with me as I hone my tutorial writing skill set.

When we last left of with our Pylons forum we had a had just successfully created a post, and then could immediately retrieve that same post. However, we kind of skimped on the testing story so lets fill in the gaps and do some refactoring as a bonus.

Functional Testing Models

Open your functional controller test located at pylonsforumtestsfunctionaltest_home.py and add the following import line

from pylonsforum.model import Thread, Post, meta

and the following methods

def setUp(self):
    meta.Session.remove()
    meta.metadata.create_all(meta.engine)
    self._createpost(5)

def _createpost(self,times):
        i = 0
        while i < times:
            i = i +1
            thread = Thread()
            thread.subject = “subject ” + str(i)
            post = Post()
            post.author = “posting away”
            post.content= “”
            post.isparent = True
            thread.posts.append(post)
            meta.Session.add(thread)
        meta.Session.commit()

 

and change the test_recent_posts method to

 

   def test_five_most_recent_threads_show_in_homepage(self):
        response = self.app.post(url(controller=“home”, action=“index”))
        assert “subject 1″ in response
        assert “subject 2″ in response
        assert “subject 3″ in response
        assert “subject 4″ in response
        assert “subject 5″ in response

 

 

In summary here we’re adding a _createpost factory method that generates some basic test threads and parent posts. running nosetests –-with-pylons=test.ini should result in failed tests.

Changing The View and Controller

Now adjust your index.mako view to the following:

<%def name=“title()”>Pylons Forum</%def>
<%def name=“head_tags()”></%def>
<div id=“recentposts”>
<table>
<thead><tr><th>subject</th><th>author</th><th>date submitted</th></tr></thead>
<tbody>
%for t in c.threads:
    <tr><td>${t.subject}</td><td>${t.parentpost.author}</td><td>${t.dateadded}</td></tr>  <!– changed posts properties to thread properties and added parentpost –>
%endfor
</tbody>
</table>
</div>

So once you’ve changed the to using a “threads” object, added parentpost (where did that come from? I’ll get to that in minute) and changed our properties around we need to change the our home.py controller index action for our cosmetic changes and to work with our new tests:

 def index(self):
        c.username = “rsvihla”
   # removing this line    c.posts = [Post("jkruse", "New Kindle", "06/24/2009")]
        thread_query = meta.Session.query(model.Thread)
        c.threads = thread_query.order_by(model.Thread.dateadded.desc()).limit(5).all()
        return render(‘index.mako’)

So our index action now is querying for the 5 most recent threads and storing in our new c.threads context variable. Lets strip the query down into pieces.

thread_query = meta.Session.query(model.Thread)

we’re working with SQLAlchemy Session object. We call a Thread Model and then store a Thread query object inside the thread_query variable.  Now through the thread_query variable we can get  access to the Thread objects stored in the database.

c.threads = thread_query.order_by(model.Thread.dateadded.desc()).limit(5).all()

Here we call “order_by” on our query object and then pass in the Thread model specifying a descending order of the dateadded property. Next we limit it to 5 rows and then call “all()” which returns our results in a nice python “list” object.

Custom Properties on the Model

Ok so back a few paragraphs ago in the index.mako view I stuck in a “parentpost” property on the thread, and I’m quite certain you’re wondering how I did that. So I’ve created the following class teststest_model.py

from pylonsforum.model import Thread, Post, meta
from pylonsforum.tests import *

class TestThreadParent(TestController):

    def _makethread(self, hasparent):
        thread = Thread()
        thread.subject = “test”
        parentpost = Post()
        parentpost.content = “”
        parentpost.author = “first post”
        parentpost.isparent = hasparent
        childpost = Post()
        childpost.content = “”
        childpost.author = “”
        childpost.isparent = False
        thread.posts.append(childpost)
        thread.posts.append(parentpost)
        return thread

    def test_should_find_parent_post(self):
        thread = self._makethread(True)
        assert thread.parentpost.author == “first post”

    def test_should_not_find_parent_when_none_set(self):
        thread = self._makethread(False)
        assert thread.parentpost is None

Our test has a _makethread factory method (another one of those maybe time for a factory class soon!) which can make a parent post or not depending on its parameter, then two tests documenting its behavior in each situation.

So I’ve gone back to our models__init__.py class and changed the Thread class to look like this:

class Thread(object):
    @property
    def parentpost(self):
        for p in self.posts:
            if p.isparent == True:
                return p

 

Not the best method however its VERY interesting for static language veterans. We are searching an instance variable that we have not even defined!  Remember only our SQLAlchemy mapper even makes mention of a “posts” variable through our relationship mapping, yet our “parentpost” property is searching through self.posts.  Once it finds a parent post it returns true, this is not perfect and again not the ideal way to enforce a database constraint but for a demo it works fine.

Calling nosetests –-with-pylons=test.ini should now give you all passing tests. Running paster serve –-reload development.ini and opening http://localhost:5000 , adding a couple of threads and you should have something that looks like this:

Picture 1

Summary

We’ve completed a home page now that has some dynamic data, added a custom property on our database class, and addressed functional testing with models. 

Related Articles:

    Post Footer automatically generated by Add Post Footer Plugin for wordpress.

    About Ryan Svihla

    I consider myself a full stack polyglot, and I have been writing a lot of JS and Ruby as of late. Currently, I'm a solutions architect at DataStax
    This entry was posted in Pylons, Python, SqlAlchemy. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
    • Govind

      Sorry Ryan – one more nitpick, unless we add datecreated column to the model – the query c.threads = thread_query.order_by(model.Thread.dateadded.desc()).limit(5).all() will not work.

      So a fix needs to be _init_.py
      sa.Column(“dateadded”, sa.types.TIMESTAMP(), default=now())
      for threads

    • http://www.lostechies.com/members/rssvihla/default.aspx Ryan Svihla

      @Govind again excellent catch. I had meant to add that back to the previous article and had never gotten around to it.

      I added the mapping to the previous article so hopefully its all correct now.

    • http://www.buresund.se/ Roland Buresund

      Your index.mako needs:
      < %inherit file="/base.mako"/>
      at the top to work