Testing couchapps with couchdb

Akshat Jiwan Sharma, Wed Sep 17 2014

Are you using couchapp or erica for managing couchdb design documents? Do you feel a bit dirty every time you push a new design document to couchdb without testing it first? And are you tired of feeling dirty all the time? Well then this article will teach you how to clean your couchdb design docs in a fragrant test solution before pushing it to your production server so you can be sure that all your production code is clean
and shiny or something along those lines....

What are we doing?

We will write simple tests using couchdb show and list functions to test
parts of couchdb design docs. We will run these functions on our local couchdb instance before pushing it to the production server. Then we will lay out a formal plan for designing CABS. No not those cabs silly! Couchapp Automated Build Systems.

Show and list?

Yes sir. Show and lists functions are a terribly underrated feature of couchdb. The purpose of show function is to transform a document into a format that is consumable by the client. Like suppose transforming a json document into an html page so that it could render nicely on a web browser.

Similarly a list function was designed to transform the output of views into a format that is consumable by a client.

But here is the thing. Show and list functions can be used to execute arbitrary server side code. Well almost... there are only a few built in functions that show and list are allowed to use but even it's limited power gives us tremendous flexibility. We are going to make use of this to run server side tests.

It's time for some action

Okay Enough talk now we are going to start by testing a view function. Consider this view

function(doc){
    if(doc.created_at){
    emit(doc.created_at,null);
    }
}

It is pretty straight forward. It just sorts the documents by the created_at field. Let's write a list function to test this.

function(head,req){
    var row = getRow();
    var test_results = [];
    var is_correct_view = req.path.indexOf("general")!==-1;
    if(!is_correct_view){ return toJSON({"failed":"test performed against the wrong view"});}
    while(row){
    var pass_condition = new Date(row.key*1000).getTime()>0;
    if(!pass_condition) return toJSON({falied:"unexpected result"});
    test_results.push(true);
    row = getRow();
    }
    return toJSON({"passed":"view tests passed:"+test_results.length.toString()}); 

}

What this list function does is:- it checks that the documents emitted by the the view contain a key that is a valid date. If the date is valid the test passes and if it is invalid the test fails. Some points to note here

In line #3

if(!is_correct_view){ return toJSON({"failed":"test performed against the wrong view"});}

we check if the list is being executed against the correct view. Remember that list functions can be executed against any view in the same or different design document. While this is certainly a handy feature we only want our test cases to execute against the views for which they were written. Consequently the test will fail if it is attempted to be executed against a different view.

In line number #7

if(!pass_condition) return toJSON({failed:"unexpected result"});

We fail the test if even one document does not pass. This might sound stringent but it is not really. A view should index a document only if it passes the map function. Our map function indexes the document only if it contains a date. If the indexed document has no date then it should not have been indexed at all. Hence the result must fail. We can probably do better by outputting the docs for which the test fails. But you get the idea.

Otherwise we output a test passed message along with the number of documents the test was run against.

To run this test we simply call this list function. In case you have a large number of documents in your couchdb database you may wish to test only a subset of those. Couchdb allows you to limit the number of results that is emitted by the view. To limit the number of documents as an input to the test function just use the limit=number query parameter.

Now that we have seen how to write tests for a view let us take an example of slightly more complex view:-

function(doc){
    if(doc.tname){
    var rent = parseInt(doc.rent);
    emit([doc.tcurrency,doc.temail,doc.tname,doc.created_at],rent);
    }
}

Instead of emitting a single key this view emits an array of keys and a single integer value "rent".

Let us write a test case for this view:-

function(head,req){
    var row = getRow();
    var array_equals = require("modules/array_equals").array_equals;
    var doc_keys = ["_id","_rev","created_at","tname","tcurrency","rent","temail"];   
    var test_results = [];
    var is_correct_view = req.path.indexOf("tenant_details")!==-1; 

    if(!is_correct_view){ return toJSON({"failed":"test performed against the wrong view"});}

    while(row){
    var keys = Object.keys(row.doc);
        var pass_condition = array_equals(doc_keys,keys)&& row.key.length===4;
    if(!pass_condition) {
        return toJSON({"failed":"unexpected results"});

    }

        if(pass_condition)test_results.push(true);
    row=getRow();
    }
    if(test_results.length>0){    
    return toJSON({"passed":"view tests passed:"+test_results.length.toString()}); 
    }
    return toJSON({"failed":"by default"});
}

As you can probably tell our test function has become a bit more complicated. But it still has the original ideas ingrained into it. First of all the list is only run against the view for which it was designed. In this case the "tenant_details" view. And as before
all the documents must pass this test for it be considered successful.

"So what's changed then?"

Good question. This time we are testing for the ordering of emitted keys instead of the testing for the type of emitted keys. A careful observer might have noticed line #2

var array_equals = require("modules/array_equals").array_equals;

array_equals is the function that takes in two arrays and check if they are equal or not.

array_equals([1,2,3],[1,2,3]) //true
array_equals([1,2,3],[3,2,1]) //false
array_equals([1,2,3],[1,2,3,4]) //false
array_equals([1,2,3],[a,b,c]) //false

with this in mind I created a

var doc_keys = ["_id","_rev","created_at","tname","tcurrency","rent","temail"];

doc_keys variable and tested that: In each row the document contains object properties equal to the previously defined doc_keys array.

One main difference though is in how we run this test. Along with a limit=number we also pass in a include_docs=true parameter.

"Why is that?"

That is because in couchdb the keys in the result set are emitted as values. For instance if you have a view that :

emit(doc.created_at,null)

in the result set you will get

1410953047410

That is the actual value of the emitted key: you don't know what property of the doc is being emitted. So as a workaround to this we fetch in the original document,extract the properties out of it and simply compare if it contains the right keys.

It should be noted that this is not a general solution. This works for me because I have only one view that indexes one type of document. It is not uncommon to have multiple views that index the same document in different ways. But the strategy remains the same. You have to test the results returned by your view. Whether you test if the type of key or if the document contains valid properties, or if the order of keys is as expected it does not matter.

"Is there any point to testing the views? "

Good question once more. Yes and no. Having a test for views is during development
absolutely invaluable as it quickly allows you to make changes to your functions while being confident that correct results are being returned. Yes it will need more maintenance on your part as you will have to update the tests as well as the views but I think that the effort is minimal as compared to the peace of mind it brings.

For instance I am thinking of changing my

emit(doc.created_at,null)

view to

emit([year,month,day],null)

This means that I will have to change my test list function to re-assemble this array into a valid date. It is pretty simple. I can use the built in date functions to convert this array into the time stamp and then compare the value with the doc.created_at that I get from the include_docs parameter.

But to be honest testing views is pretty much useless in production. Why? Because production views should be static. If you are running tests on production views it means that they are changing too often which means that you are doing something wrong. That does not mean that testing is useless because:-

It is not just the views that you can test.

Remember that show and list functions can execute arbitrary server side code. So besides views you can test any common js modules. You can even export your show and list functions and test them in other show and list functions!

But probably the best reason to test with couchdb is that you don't need a mock environment. You test against the data that is real and the one against which you are already developing the application. That is a pretty big win if you ask me.

Automation as promised

Once we have test cases written it is pretty easy to automate it mainly because couchdb already provides a nice http api to execute the tests (or call the list functions). Here is what we want to do:-

  1. Push the design doc locally
  2. Run all the tests.
  3. If all the tests pass push the doc to the server.

Now if you are using erica or couchpp you are probably already familiar with the .couchapprc file where you define the local and prod databases. This makes our job a lot easier. Here is the plan.

  1. Pick any automation tool you like. Grunt? buildify? shell script?
  2. After changing your design docs run an os command couchapp push (or erica push) from the script.
  3. Once the doc has been push to couchdb make a series of http requests to list or show functions. You can easily store the urls in an array.
  4. If all the calls result in pass Run another os command couchapp push prod (erica push prod) to push your design doc to the production database.
  5. If a call results in failed stop the tests and print out a message on the console.

Pretty simple no.

Closing

Good tests are hard to write. The intention of this article was not to show you how to write good tests. To be honest I don't know how to write good tests as well. But I know that some tests are better than no test. And just like your code your testing strategy can be improved over time as you understand your own application better. Couchdb gives you some pretty powerful tools to test your application. Exploit them!

You will also like the official couchdb blog and this other article that talks about using couchdb for user accounts management

See you space cowboy


comments powered by Disqus