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:
- For each JSON document format you have in your database, you define one or more example JavaScript objects that conform to these formats.
- For each test case you redefine
emitTester
function to actually contain the tests (assertions) - 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
andtwitterId
.
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
andfacebookId
.
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.