Friday, August 12, 2011

From the frontline, day 5

Another day, another piece of testing mayhem. I've completed the 0.1 version of my Flask-Downloader helper class. With this, I could complete my web app. Now, the downloader itself has a bunch of tests to make sure it's working as expected, but I was also going to test the corresponding code paths in the web app's tests.
The user can provide the input either by uploading a file or by giving an accession number. Testing for the file uploads was easy, as the Flask test client accepts file-like objects as data input for POST requests. So testing the app will do the right thing is as easy as:
def test_upload(self):
    file_handle = open(tmp_filename)
    data = dict(file=file_handle)
    rv = self.client.post('/upload', data=data)
    assert "upload succeeded" in rv.data
Assuming your upload function listens on '/upload' and returns a page that contains "upload ducceeded", of course.
Testing file downloads is a bit more elaborated, because I don't actually want my downloader to connect to the internet during a test run. Minimock to the rescue! I can fake the download helper and create the same kind of output to fool the application code.
from minimock import Mock
from werkzeug import FileStore
def test_download(self):
    data = dict(id="FAKE")
    # now create the fake downloader
    tmp_file = open(tmp_file_path)
    dl.download = Mock('dl.download')
    dl.download.mock_returns = FileStore(stream=tmp_file)
     rv = self.client.post('/download', data=data)
    assert "download succeeded" in rv.data
With similar assumptions as in the example before, and also the idea that you have a pre-existing file in tmp_file_path. A StringIO file-like object should do the trick as well.
With all the tests in place and a test coverage of 100%, I declare this campaign a success. I still need to deploy the new web app on my test server instead of the old one, but I'm going to do that next week. I will also continue my war on legacy code, now tackling the pieces that do the actual work. No war is over as quick as you'd initially hope after all. Also, I'm pretty sure the 100% code coverage don't mean there's not plenty of places for bugs to hide in, just that at least all of the code is looked at by the interpreter once. Still, it's a good conclusion to a busy week. Testing rocks.

Thursday, August 11, 2011

From the frontline, day 4

Today, I decided to go for the downloader component that can download files on the behalf of the users. While looking at how to test this, I actually noticed that the mm_unit functionality has been merged into the minimock package. Sweet.
I wanted to keep this modular, a downloader sounds like a tool I could use in a couple of projects. So I created a Flask extension. There's a nice wizard script that automates the creation of the boilerplate files. Using the wizard, I created Flask-Downloader. It's pretty straightforward to use. There's a download(url) function that will return a werkzeug.FileStorage instance, just like the flask upload hander. I'll also add a save(url) function that'll save the url's contents to a file without returning a file-like object.
Not too much to write about, spent a lot of time researching stuff today. Hope to get done with my changes tomorrow. Let's see how that'll work out

Wednesday, August 10, 2011

From the frontline, day 3

Today, I decided to go and restructure my webapp into a package as recommended by the Flask "Larger Applications" pattern. Thanks to my existing test suite, the move was quick and pain-free. I had to fix imports in one of the tests, but apart from that, the only thing I had to do was splitting up the webapp.py file correctly into webapp/__init__.py, webapp/views.py and webapp/models.py.
I then started playing with implementing the actual functionality for uploading files and creating database entries for the jobs submitted. Took a while to get this right, never done database stuff with Flask before. But again, pretty easy to set up tests for all this. Also, I discovered Flask-Testing, making flask unit testing even more comfy. Just had to fix up the Twill module Flask-Testing comes with to not use the md5 and sha modules, triggering deprecation warnings. Will continue to write the last tests for the job submission form tomorrow, and then see how to deal with making the web app download data from elsewhere for the user.

Tuesday, August 9, 2011

From the frontline, day 2

Looks like my system strikes back. A fitting thing to happen for an episode 2, I guess. Turns out that nosetests and virtualenv need some extra care and feeding when kept together. Installing another copy of nosetests into my virtualenv fixed the test failures I was seeing. Thanks to the folks on #python and #pocoo for pointing me the right way.
Of course this broke the code coverage. Nothing a pip install --upgrade coverage wouldn't fix, though. As an added bonus, the coverage html output now looks much nicer. I guess it was redesigned between whatever my system got and the version pip grabbed.
Of course, after spending quite some time writing tests for my email sending module, the #pocoo folks point me at the already existing Flask-Mail extension, that integrates into the Flask test harness (as in, if you're testing, it won't send email) already. Oh well. Switched, ditched quite some code and corresponding tests. Even less stuff I have to maintain myself.
Unfortunately, Flask-Mail doesn't seem to like it when you switch on app.config['TESTING'] = True after initialization. Fortunately, you can still fiddle with the value used so it doesn't try sending emails, like so:
def setUp(self):
    webapp.app.config['TESTING'] = True
    webapp.mail.suppress = True
The key here is the mail.suppress = True setting. Once that's done, all the testing options work as expected. You can even have a look at the msg objects that would have been sent using the following snippet:
def test_sent_mail(self):
    """Test if emails were generated and sent correctly"""
    with webapp.mail.record_messages() as outbox:
        rv = self.app.post('/send-email',
                 data=dict(message="hello world"))
        assert len(outbox) == 1
        msg = outbox[0]
        assert "hello world" in msg.body
I like it, this really gets all the stuff I do under test in a very straightforward manner.

Monday, August 8, 2011

From the frontline, day 1

I've decided to start the campaign by ditching the existing PHP web app. I lost all confidence in it last Friday, when I found that it only worked by accident, due to a well-placed typo.
As I'm rewriting the web app anyway, I thought I could also ditch PHP altogether. Not that it's necessarily a bad language for web apps, but the rest of the code is perl or python, so getting rid of php means one less language to get confused by.
Because this is the War on Legacy Code, I'm not going to write untested code in this campaign. So first I need to brush up my python unit testing skills. I do have parts of a python version of the web app already (untested, that won't work later), but it's missing a user feedback form.
I don't want to send an email for every run of the test suite, so I need to mock up smtplib.SMTP. After some web research, I'll be using Ian Bicking's minimock to provide my mock objects. As I don't just run doctests (even though they're pretty cool), I decided to also throw in MiniMockUnit, which makes minimock print the output to a StringIO buffer instead of stdout. That way, you can easily put it in a normal unit test.
I usually run my tests using nosetests. Turns out, nosetests allows me to run both vanilla unit tests and doctests, and it also has a code coverage plugin. Thus,
nosetests -v --with-doctest --with-coverage --cover-html --cover-package="testmodule"
will get the module "testmodule" tested using available unit tests, doctests and the test coverage will be reported in html in the cover/ directory. The --cover-package part seems to be needed to stop the coverage code from trying (and failing) to create coverage information files in the standard lib paths.
To sum up, I didn't actually see much battle.. er.. code today but my arsenal is filled with testing tools, and I'm well prepared to jump into the fray tomorrow. Also, thanks to some help from the folks on #gsoc IRC on freenode, I now have decent syntax highlighting and formatting on my blog, so I might be able to post code samples for real now. Life is good so far.

War on Legacy Code

Following the hallowed US American tradition of declaring war on whatever things you don't like, I've decided to declare war on legacy code this week.
By legacy code, I mostly mean untested code, following the definition of Michael Feathers' book Working with Legacy Code. That's a great read, by the way. If you're working with old code, you should go read it. I found that I've been doing most of the things mentioned already, but it's nice to see a systematic write-up about it.
My chosen battlefield in this war is the code at my day job, mostly because it's in a much worse shape than the code I deal with in the various Open Source projects I'm involved in. Seeing how my day job code is a Frankenstein's monster of Perl, PHP and Python parts, some of the work will be to get some of the tests done twice. In particular, I really want to get rid of the PHP parts.
I won't delve into the particulars of the code too much, it's published under the GPLv3 if anyone is interested. I will however try to post some daily news from the front lines, with things that I have thought about during that particular day of the battle.