Logo Questions Linux Laravel Mysql Ubuntu Git Menu

IndexedDB testing with Jasmine for browsers

I'm looking into testing an indexedDB setup using Jasmine (with Testacular in particular). Inside of an app I'm writing the database to be opened, created, removed, etc with no issue whatsoever. Now when I attempt to write a unit test to ensure data is being saved from a service correctly, I keep receiving errors, such as timeouts, and the Chrome resource panel (currently testing on Chrome 25) doesn't show the database created with the correct tables. What is a simple implementation involving testing indexedDB?

like image 847
Ryan Q Avatar asked Feb 17 '23 06:02

Ryan Q

2 Answers

There are a few things to keep in mind when testing indexedDB.

  • You must keep in mind the async nature of indexedDB and handle it appropriately inside of Jasmine. The key for async tests is to use some of jasmine's built in Async Spec functions like "runs", "waits", and "waitsFor". Make sure when using these methods your correctly placing them. (IE Placing a waitsFor() outside the beforeEach() may result in timeout errors.
  • Unless you wish to write your own, try using one of several wrappers with what ever works best for you.
  • Ensure your creating new transactions for each manipulation, or you will receive InvalidStateErrors.
  • If you wish to avoid using the built in waitsFor() left and right with async tests you may wish to look into something like Jasmine.async

The following is something I made to demonstrate a simple test for adding one item to the database. This should work on Chrome 24+, FF 16+, and IE10 (granted you have a version of jquery with Deferred's) without vendor prefixes. However I highly suggest using one of the aforementioned IDB wrappers / plugins. Adding more error output is likely desired here, but hopefully this helps in getting started.

describe("Database interaction", function () {

        var settings = {
            name: "TEST",
            version: 1
        var stores = [
            {name: "store1", keyPath: "id"},
            {name: "store2", keyPath: "id"},
            {name: "store3", keyPath: "id"}
        var db;

        function setupDB(){
            var dbRequest = window.indexedDB.open( settings.name, settings.version),
                dbDfd = $.Deferred();

            dbRequest.onsuccess = function( event ) {
                console.log("Opened DB");
                db = dbRequest.result;
                dbDfd.resolve( db );
            dbRequest.onblocked = function( event ){
                console.error("DB connection blocked");
            dbRequest.onerror = function( event ){
                console.error("DB connection issues");
            dbRequest.onupgradeneeded = function(){
                var i, cur;
                db = dbRequest.result;

                //Create non-existant tables
                for(i=0; i < stores.length; i+=1){
                    cur = stores[i];
                    db.createObjectStore( cur.name,  {keyPath: cur.keyPath, autoIncrement: true});
            return dbDfd.promise();

            var done = false;
            runs( function(){
                var delRequest = indexedDB.deleteDatabase("TEST");
                delRequest.onsuccess = function( event ){
                    console.log("DB Deleted");

                            console.log("DB Setup with stores: ", db.objectStoreNames );
                            done = true;
                delRequest.onerror = function(event){
                    console.log("DB Err: ", event );
                    done = true;
            waitsFor( function(){ return done; }, "Database never created..", 10000 );

        it('should add an item to store1', function(){
            var done = false;
            //Open a transaction with a scope of data stores and a read-write mode.
            var trans = db.transaction( stores.map(function(s){return s.name;}), 'readwrite');

            //"objectStore()" is an IDBTransaction method that returns an object store
            //that has already been added to the scope of the transaction.
            var store = trans.objectStore('store1');

            var req = store.add({"id": 2, "foo":"bar"});
            req.onsuccess = function(){

                //Remember to not access store or trans (from above), or this.source.prototype functions as we can only access those in a new transaction
                //Added an object to the store, expect result to be ID of item added
                expect( this.result ).toBe( 2 );
                //Some other expectations
                expect( this.source.name ).toBe("store1");
                expect( this.source.keyPath ).toBe("id");
                expect( this.source.autoIncrement ).toBe( true );
                expect( this.source.indexNames.length ).toBe( 0 );
                expect( this.transaction.mode ).toBe("readwrite");
                expect( this.transaction.db.name ).toBe("TEST");
                done = true;
            req.onerror = function(){
                console.log("Error adding object to store");
                done = true;
            waitsFor(function(){ return done; }, "Didn't add store item", 10000 );
like image 129
Ryan Q Avatar answered Feb 26 '23 20:02

Ryan Q

I've got quite a large test suite in db.js which is a IDB wrapper and it mostly relies on using the waitsFor to do the async operations. Combining this with the beforeEach and afterEach are useful as they can be used to control your setup/tare down of the DB connection, check out the specs folder if you want to see some in action.

like image 21
Aaron Powell Avatar answered Feb 26 '23 20:02

Aaron Powell