Lecture 11 -- Testing, message boards, and registration

November 10th, 2009.

No office hours next Monday. Should the homework be due next Thursday, or the following Tuesday, or what?

No class next Tuesday (I'm presenting at the Great Lakes Software Excellence Conference, glsec.org).

You may have noticed that Python is now dead?

http://python.org/dev/peps/pep-3003/

Tips and tricks: favicon.ico

Did anyone notice that 'favicon.ico' is always requested!? What is that?

Testing and code coverage

Gratuitous Darth Vader reference:

http://www.flickr.com/photos/sebastian_bergmann/2282734669/

Re testing and 100% coverage,

Why would you write this?

class FileServer(object):
   def __init__(self, dirpath):
      self.dirpath = os.path.abspath(dirpath)

   def __call__(self, environ, start_response):
       url = environ['PATH_INFO']
       url = url.lstrip('/')

       filename = os.path.join(dirpath, url)
       filename = os.path.abspath(filename)

       # ... do other stuff ...

       # make *sure* it's under dirpath
       if not filename.startswith(self.dirpath):
          raise Exception("bad web user!")

       content = open(filename).read()

       return [content]

What is the point of that last 'if'?

(for later, when other developers, or you, mess with the code!)

...and how would you test the line of code that raises the exception?

A few ideas:

  • pass in a purposely messed-up URL in PATH_INFO.

  • make os.path.abspath return something weird the third time it was called -- this is called dependency injection, where you replace a perfectly good function with a different one for the purposes of testing.

  • change the code to make it more testable ;)

    In this case, a straight out 'assert' might work, wrapped in a function:

    def get_file_content(self, url):
      url = url.lstrip('/')
    
      filename = os.path.join(dirpath, url)
      filename = os.path.abspath(filename)
    
      # ... do other stuff ...
    
      # make *sure* it's under dirpath
      assert filename.startswith(self.dirpath)
    
      content = open(filename).read()
      return mimetype, content
    

    ...but then you've got to trap the assertion error:

    def __call__self(self, environ, start_response):
      url = environ['PATH_INFO']
    
      try:
         mimetype, content = self.get_file_content(url)
      except AssertionError:
         ... return 500 ...
      except IOError:
         ... return 404 ...
    
      ... return 200, file content ...
    

    ... and really you've just pushed the testing problem up a level!

    Or have you?

    First, you've decoupled 'get_file_content' from the WSGI serving mechanism (which is good); you can test the file stuff separately from the WSGI stuff.

    Second, you can more legitimately use the dependency injection trick here:

    def test_500():
       def broken_get_file_500(url):
          raise AssertionError("foo")
    
       app = FileServer(dirpath)
       app.get_file_content = broken_get_file  # REPLACE FUNCTION DYNAMICALLY
    
       ...
    
       environ['PATH_INFO'] = '/foo'
       data = app(environ, start_response)
    
       assert start_response.status.startswith("500 ")
    

    So, literally, you are dynamically swapping out a method on an object... neat, eh?

More on dependency injection

You can replace any pure-Python attribute/function/method with your own, just by assignment.

It is an excellent way to get yourself into trouble, however, because you may forget to replace. A good mental habit: use try/finally.

try:
  orig_abspath, os.path.abspath = os.path.abspath, effed_up_function

  # ...

finally:
  os.path.abspath = orig_abspath

'finally' blocks are guaranteed to be executed even if an unhandled exception is raised inside the try block.

A few examples to go over:

>>> class A(object):
...    def f(self):
...       self.g()
...    def g(self):
...       print 'I am a manly man'

>>> a = A()
>>> a.f()
I am a manly man

as expected!

Now, define a function to replace 'g':

>>> def h(param):
...    print 'I am a womanly woman'
>>> a.g = h
>>> a.f()
I am a womanly woman
>>> print a.f
<bound method A.f of <__main__.A object at 0x83e70>>
>>> print h
<function h at 0x88030>
>>> print a.g
<function h at 0x88030>
>>> m = a.f
>>> m()
I am a womanly woman
>>> h == a.g
True

Note:

  • 'h' is not a bound function, which means it's not bound to 'self', so it doesn't take 'self' as an argument. (i.e. 'self' is treated as "part of the function"; see m, above).
  • you can do 'print' and equality comparisons between functions, which is good for debugging purposes.

When you do this kind of ^#%!%! to production code (i.e. not just for testing), it's called "monkey patching".

Don't. It makes your code undebuggable.

Using it for testing is legitimate, however.

It's also worth thinking about how much more code is untestable in static languages... Remember, B&D is good for you! (?)

The larger lesson is, always think about how you might test your code (and always test it -- Darth Vader reference #2).

It will often lead to better code, too.

Building a message board

So, let's build a message board!

Registration

Typical registration flow (message board).

  • can't post if you're not logged in
  • log in
    • existing account? username/password please!
    • create account:
      • enter username, password, hair color, mother's maiden name, etc.
      • create
      • go log in

How would this work "behind the scenes", i.e. software?

(Let's assume that you can assign a cookie (limited to a key into a dictionary or database) to each user uniquely.)

  • when to assign
  • when to clear
  • is assigning == authenticating?
  • tying it to an IP address?

Back to the message board

What features should/could message board have?

  • posting messages
  • replying to messages
Q:
  • methods for moderation and spam filtering
  • how do you prevent automated signups?
  • ??

Think about it in context of an online store, too...??

How does Amazon do registration? (think: recommendations; wish lists; buying)

Google?

---

For Thursday, read this registration article:

http://www.asktog.com/columns/081Registration.html

and think about what kind of registration system you'd use for your message (assume that it's for-profit, you care deeply about subscribers, etc etc)