Pragmatic Geographer

SOFTWARE GEOGRAPHY ECONOMICS HISTORY

Testing Search (Haystack) in Django

Django’s build-in testing framework is extremely handy. As long as you use the ORM with a supported data store, a test database is used for the duration of the tests and is cleaned up in between unit tests. There is no need for elaborate mocking – something I had grown accustom to in .NET.

Here is a quick sample, edited for brevity:

1
2
3
4
5
6
7
8
9
10
11
$ ./manage.py test appname -v 2

Creating test database for alias 'default' ('test_projectname')Syncing...
Creating tables ...
test_first (projectname.test.SampleTestClass) ... ok
test_second (projectname.test.SampleTestClass) ... ok
test_third (projectname.test.SampleTestClass) ... ok
Ran 3 tests in 1.260s
OK
Destroying test database for alias 'default' ('test_projectname')

But if you are using some external source of data, it is necessary to create a mock or some fake environment (as Django does).

Haystack is a handy library that abstracts out the details of various search engines. You get some powerful features build into something like Elasticsearch – high availability, full text search, spelling correct, more like this, etc – in some functions and data structures familiar to Django using developers.

But if you are integration testing, and you should be – the tests are calling your views directly and your views are updating or retrieving data from an external search engine, you are going to potentially have a bad time. Stuff will be persisted between unit tests and your results will be likely be inconsistent.

The solution is pretty simple actually. Fire up a new index, override the settings such that the new index is the target for the Haystack calls for the duration of tests, and clear the index between tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TEST_INDEX = {
    'default': {
        'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
        'URL': 'http://127.0.0.1:9200/',
        'TIMEOUT': 60 * 10,
        'INDEX_NAME': 'test_index',
    },
}


@override_settings(HAYSTACK_CONNECTIONS=TEST_INDEX)
class BaseTestCase(TestCase):

    def setUp(self):
        haystack.connections.reload('default')
        super(BaseTestCase, self).setUp()

    def tearDown(self):
        call_command('clear_index', interactive=False, verbosity=0)

Gist

Comments