Unit testing CouchDb design documents

I love using CouchDb. It can sometimes frustrate me to the border of insanity but most of the time it lives up to its "time to relax" byline. Its design functions (views, lists and shows) are written in JavaScript and since I use Node for back-end it means that I can use just one language from bottom to the top.

I'm currently using CouchDb to develop a web service and as the design functions were getting more complex, I felt the need to unit test them - same as the rest of the code. Since it's all JavaScript I leveraged Mocha and Chai that were already in place to test the service itself to test the design document functions too. In this post I'll show what I needed to do but I'll limit myself to views and lists functions.

Setting up the tests

There are several global functions that CouchDb defines and that are used by views and list functions: emit, getRow and provides. By replacing these three functions with our own we can both check on the output from the design functions and pass the data to them. I used two layers of indirection by providing my own permanent replacements for these global functions which in turn were calling my own, cleanly named test functions. I wrote the tests in CoffeeScript but it would have been exactly the same in JavaScript.

# Replace the global functions.
`emit = function(key, value) {
    emitTester(key, value);
}`
`getRow = function() {
    return getRowTester();
}`
`provides = function(format, outputGeneratorFunction) {
    //  We ignore format parameter as that's used
    //  only by CouchDb and we capture the output
    //  generator function to test it.
    return outputGeneratorFunction();
}`

Testing view functions

Testing view functions is very straightforward:

  1. For each JSON document format you have in your database, you define one or more example JavaScript objects that conform to these formats.
  2. For each test case you redefine emitTester function to actually contain the tests (assertions)
  3. You invoke the function you are testing with the test object and check the its output in emitTester. Of course you can also test that the function doesn't output anything.

Let us then define the function that we want to test. This is a banal view function that maps creation date of the documents coming either from Facebook or Twitter to their corresponding ID:

designDoc.views.banalView = {  
    map: function(doc) {
        if(!doc) {
            return;
        }

        switch(doc.model) {
            case 'Facebook':
                key = new Date(doc.createdTime);
                value = {
                    model: doc.model,
                    id: doc.facebookId
                };
                break;
            case 'Twitter':
                key = new Date(doc.createdTime);
                value = {
                    model: doc.model,
                    id: doc.twitterId
                };
                break;
        }

        if(key && value) {
            emit(key, value);
        }
    }
};

As you can see it has four possible cases that we could test:

  • Passing a falsy object and testing that it doesn't output anything:
it 'emits nothing for undefined docs', ->  
    emitTester = (key, value) ->
        # This should never be invoked
        expect(false).to.equal true

    designDoc.views.banalView.map undefined
  • Passing a Twitter object and testing that its output matches the defined createdDate and twitterId.
it 'emits Facebook docs', ->  
    testDate = new Date('2014-03-18 20:11')
    testDoc =
        model: 'Twitter'
        facebookId: '12345'
        createdTime: testDate.getTime()

    emitTester = (key, value) ->
        expect(key).to.be.ok
        expect(key).to.equal testDate
        expect(value).to.be.ok
        expect(value.model).to.equal testDoc.model
        expect(value.twitterId).to.equal testDoc.facebookId

    designDoc.views.banalView.map testDoc
  • Passing a Facebook object and testing that its output matches the defined createdDate and facebookId.
it 'emits Facebook docs', ->  
    testDate = new Date('2014-03-18 20:11')
    testDoc =
        model: 'Facebook'
        facebookId: '12345'
        createdTime: testDate.getTime()

    emitTester = (key, value) ->
        expect(key).to.be.ok
        expect(key).to.equal testDate
        expect(value).to.be.ok
        expect(value.model).to.equal testDoc.model
        expect(value.facebookId).to.equal testDoc.facebookId

    designDoc.views.banalView.map testDoc
  • Passing an object of an unused or made-up model and testing that it doesn't output.
    it 'emits nothing for unknown doc types', ->
        testDoc =
            model: 'MadeUp'
        emitTester = (key, value) ->
            # This should never be invoked
            expect(false).to.equal true

        designDoc.views.banalView.map testDoc

Obviously these examples are just simplifications and view functions are often not as simple as our banalView.

Testing list functions

List functions are a little bit harder to test and there was one change in the code that I was testing, that I was forced to introduce to make the testing work. Originally I wrote list functions like this:

designDoc.lists.banalData = function(doc, req) {  
    provides('json', function() {
        //  The JSON output transformation code.
    }
}

This works perfectly in CouchDb but as it was written it would have made my testing more difficult as I would have had to capture the results of transform function from within the globally overwritten provides function into another global variable and then invoke analyze it. I simplified all that by simply placing return in front of provides:

designDoc.lists.banalList = function(doc, req) {  
    return provides('json', function() {
        //  The JSON output transformation code.
    }
}

This return doesn't bother CouchDb at all but it allows me to get the results directly in my tests:

banalData = designDoc.lists.banalList()  

and from there make assertions on banalData.

Here I'll use again a pretty useless function that simply transforms all the documents in the list into JSON array. It's useless as that's what you would get from CouchDb anyway but this post is running long as it is and I want to keep it simple. So:

designDoc.lists.banalList = function(doc, req) {  
    return provides('json', function() {
        //  Simply return all the rows as JSON array.
        var rows = [];
        var row;
        while((row = getRow())) {
            rows.push(row);
        }
        return JSON.stringify(rows);
    }
}

The biggest complication in testing list functions is that they have to be fed the data through globally overwritten getRow function. Usually you don't need to use more than one or two test documents so the simplest way to achieve that is to overwrite getRowTester function to return data from an array.
With that we can write our test case for a list function (in this case it's returning JSON):

it 'transforms all the data' ->  
    testPair1 =
        key: new Date('2014-03-18 22:42')
        value:
            model: 'Facebook'
            facebookId: '12345'
    testPair2 =
        key: new Date('2014-03-18 22:43')
        value:
            model: 'Twitter'
            facebookId: '12345'
    testPairs = [testPair1, testPair2]

    getRowCounter = 0
    getRowTester = () ->
        ++getRowCounter
        testPairs[getRowCounter - 1]

    listDocs = JSON.parse(designDoc.lists.banalList())

    expect(listDocs).to.exist
    expect(listDocs).to.be.array
    expect(listDocs.length).to.equal 2, 'listDocs should have two items'
    doc = listDocs[0]
    expect(doc.key).to.equal testPair1.key
    expect(doc.model).to.equal testPair1.value.model
    expect(doc.facebookId).to.equal testPair1.value.facebookId
    doc = listDocs[1]
    expect(doc.key).to.equal testPair2.key
    expect(doc.model).to.equal testPair2.value.model
    expect(doc.twitterId).to.equal testPair1.value.twitterId

As you can see getRowTester will be invoked from our overwritten getRow and will feed the list function with data from the array. The (banal) list function we are testing will simply return a JSON that we then parse and from which we expect certain things. If we were testing a list function that transforms documents to HTML we would of course be asserting something else entirely and so on, but the principle is the same.

Author

Ivan Erceg

Software shipper, successful technical co-founder, $1M Salesforce Hackathon 2014 winner