← Back to team overview

launchpad-dev team mailing list archive

Lighter tests with FakeLibrarian

 

Hi all,

This is to let you know about something I (mostly) cooked up in Prague. The FakeLibrarian is a lightweight substitute for the Librarian that you can use in single-process tests.

== Why use this? ==

Weight. The test framework no longer needs to set up the Librarian at the beginning of a test run (or verify that it is running). Your test no longer speaks http to a daemon, your data is no longer going to the filesystem, and so on.

Simplicity. Things do go wrong sometimes when you run the Librarian persistently, and getting things back up on their feet can be a nuisance. FakeLibrarian does it all in-memory.

Helpfulness. Ever have a test break because you forgot to commit after adding a file to the Librarian? The real librarian says "I couldn't find something." The fake one says "I have your file but you forgot to commit."

== How does it work? ==

You set up a fake librarian in your test setup, and uninstall it during teardown. Your test proceeds as if you had the normal Librarian running: getUtility(ILibrarianClient) etc. still work, but return the fake librarian instead of the real one.

The LibraryFileAlias and LibraryFileContent objects are still really in the database as before, so you can still join and query them.

== How do you use it? ==

Your old test may have looked something like this:

class TestKitchenAppliance(TestCaseWithFactory):

    layer = LaunchpadLayer

    def setUp(self):
        super(TestKitchenAppliance, self).setUp()
        other_setup()

    def tearDown(self):
        other_teardown()
        super(TestKitchenAppliance, self).tearDown()

    def test_prepare_recipe(self):
        # The prepare_recipe function takes a LibraryFileAlias.
        recipe = "Dice onion with extreme prejudice."
        cookbook_alias = getUtility(ILibrarianClient).addFile(
            'recipe.txt', len(recipe), StringIO(recipe), 'text/plain')
        # Needed to keep Librarian happy.
        transaction.commit()

        result = prepare_recipe(cookbook_alias)

        self.assertEqual(True, result)

Here's how you would convert that test to the fake librarian:

* Import:
from lp.testing.fakelibrarian import FakeLibrarian
* Optionally downgrade the layer to something lighter, e.g.:
    layer = DatabaseFunctionalLayer
* In setUp, create a FakeLibrarian and substitute it for the real one:
        self.fake_librarian = FakeLibrarian()
        self.fake_librarian.installAsLibrarian()
* In tearDown, remove the FakeLibrarian:
        self.fake_librarian.uninstall()
* Optionally replace transaction.commit() with:
        self.fake_librarian.pretendCommit()
* Don't forget to remove the "import transaction" at the top.  :-)

The single FakeLibrarian object implements all Librarian-related interfaces and utilities you're likely to need.

For examples of the FakeLibrarian in action, see:
 - lib/lp/testing/tests/test_fakelibrarian.py
 - lib/lp/translations/utilities/tests/test_file_importer.py

In test_fakelibrarian.py you'll see the same tests applied to both the real librarian and the fake one. That's where we test how lifelike the FakeLibrarian is.

== When doesn't it work? ==

Of course "lightweight" comes at a price.  There are limitations:

In-process. You can't use the FakeLibrarian to hand files from one process to another, or set up files in your test and then access them from a script you run.

Non-http. The FakeLibrarian will generate the same URLs for your files as the real Librarian does, but you can't access those URLs. There's no remoteAddFile either.

Incomplete. There are some more intimate Librarian APIs that the FakeLibrarian doesn't support. I don't think they're used much though.

And of course you should not use the FakeLibrarian for testing behaviour of the real Librarian. But you figured that out already. :)

== So does it?  Work, I mean? ==

It does for me. I tried converting one test case and got roughly a 20% speedup, both for setup and for running the tests. That's compared to a real Librarian and memcached that are already running before the test starts.


Jeroen



Follow ups