Geeks With Blogs

News Please visit me at my new blog!!

profile for Aligned at Stack Overflow, Q&A for professional and enthusiast programmers
"free in Christ Jesus from the law of sin and death." Romans 8:2 (ESV) Check out the Falling Plates video on YouTube.
more about the Gospel
And then listen to Francis Chan speaking at LifeLight in SD.



Programming and Learning from SD

I’ve been using Jasmine JavaScript testing library for Behavior Driven Development (see my Benefits of BDD post)  testing the last few months with KnockoutJs view models and the Revealing Module Pattern. We now have 244 specs on a system that will be around and hopefully expand. I was hired to get it going, then others will maintain it and add to it after I’m gone. They have helped me think through what needs to happen (when {X} it should do this), test functionality with out having to click through things and help stop me from breaking other things, and will be a good spec document for those that follow me. It has taken more time, in some ways, but I feel it has been more than worth it. If there is a complex scenario (example: add this, delete that, add that, assert this) it saves me a lot of clicking and re-checking and missing that I broke.

Here are some of the things and hints I’ve learned from the experience. I’ll add to this as I learn more.

To use Jasmine to it’s full potential get familiar with the Behavior Driven Development mentality. It doesn’t replace Test Driven Development, it makes it closer to what it’s supposed to be: creating solutions for customers.

Organization

  • As you add JavaScript files, add them to your Specrunner.html file and in the right order. An alternative to this annoyance, is to change to a cshtml in Razor and re-use your bundles or use RequireJs to add the dependencies (if you’re already using RequireJs).
  • Create one Spec JavaScript file per view model. If you’re working with the interaction of 2 or more view models, create a file for that as well. I’ve been naming mine {x}Specs.js. I place these in my Scripts/jasmine/specs directory.
  • Use beforeEach to setup the initial state. Use a global variable for re-use in the specs.
  • Add dispose methods on initially instantiated (‘static’) classes and clean up in afterEach. This helps make a clean state for each test. The test should not rely on a preview test’s state.
  • Limit your asserts in each spec/test. This leads better named specs. On the other hand it may result in duplicate code to setup scenarios, but it can also be refactored into common methods.
  • Use expect(5).toBe(5) instead of expect(7 === 7).toBeTruthy(); to get better fail messages.
  • Use the jasmine.Any(Object) or jasmine.any(string) etc. when calling toHaveBeenCalledWith and when you don’t care what the value of the type that was used.
  • Use expect(“this needs”).toBe(“completed”) to fail specs that you haven’t completed yet. The output is Error: Expected 'this needs' to be 'implemented'. (one approach is to write out all your specs before writing code, then when they all pass you know you’ve covered all the specifications).
  • Use the SpecHelper.js class for common functions for your tests. Refactor your tests as you see duplicates appearing or use the beforeEach method.

 

Make it Testable

  • Use the bind method for Jasmine toThrow Tests
  • Use wrappers to make your code testable.
  • Use ajax data service classes to de-couple code and make it more usable. Check out the SPA template in Visual Studio 2012.2 or John Papa’s ‘Hot Towel’ template to see the idea using Breeze.js.
  • SpyOn or fake out all service calls in the beforeEach (there should be no real calls to a web service in your tests), then turn them on by overriding the spyOn in the individual specs as you need to test that area. That keeps your tests compartmentalized, so you’re only testing the code you care about. Think of it as having all the door shut, then testing each room at a time as you open the door, instead of all the doors open and running through the house wildly. beforeEach(function(){ spyOn(dataService.UiDataService, "GetComponents"); }); See my post for an example of a data service.
  • Some more helpful wrappers:
    namespace.Utilities = {
        RedirectWindow: function (url) {
            'use strict';
            window.location = url;
        },
        OpenInNewTab: function (url) {
            'use strict';
            // http://stackoverflow.com/questions/5141910/javascript-location-href-to-open-in-new-window-tab
            window.open(
                url,
                '_blank' // <- This is what makes it open in a new window.
            );
        },
         WindowSetInterval: function (callback, interval) {
            'use strict';
            return window.setInterval(callback, interval);
        },
         WindowClearInterval: function (intervalId) {
            'use strict';
            window.clearInterval(intervalId);
        },
         WindowSetTimeout: function (callback, interval) {
            'use strict';
            return window.setTimeout(callback, interval);
        },
         WindowClearTimeout: function (timeoutId) {
            'use strict';
            window.clearTimeout(timeoutId);
        },
        IsNullOrUndefined: function (x) {
            'use strict';
            if (x === undefined) {
                return true;
            }
            if (x === null) {
                return true;
            }
            return false;
        },
         SafeKoGet: function (prop) {
            'use strict';
            if (ko.isObservable(prop)) {
                return prop();
            }
            return prop;
        };
    }

 

More Advanced

// from SpecHelper.js, I wrote this in September 2013, so is there a built in way to handle promises now?
// promise helpers
// http://www.carlosble.com/2013/03/unit-testing-javascript-with-promises-and-jasmine/
// this is just a constant, a string.
endOfPromiseChain: 'empty',
// And this is a function to ease testing:
RegisterPromiseError: function (target, err) {
    'use strict';
    if (err !== SpecHelper.endOfPromiseChain) {
        target.errorInPromise = err.toString();
    }
},
AssertAsyncToHaveBeenCalled: function (promiseSpy, target, additionalExpectation) {
    'use strict';
    waitsFor(function () {
        return promiseSpy.called || target.errorInPromise;
    }, 50);
    runs(function () {
        // this tells me if there was any unhandled exception:
        expect(target.errorInPromise).not.toBeDefined();
        // this asks the spy if everything was as expected:
        expect(promiseSpy).toHaveBeenCalled();
        // optional expectations:
        if (additionalExpectation) {
            additionalExpectation();
        }
    });
},
AssertAsyncToHaveBeenCalledWith: function (promiseSpy, target, haveBeenCalledWithArgs) {
    'use strict';
    waitsFor(function () {
        return promiseSpy.called || target.errorInPromise;
    }, 50);
    runs(function () {
        //var i,  stringedArgs = '';
        promiseSpy.argsForCall = haveBeenCalledWithArgs;
        // this tells me if there was any unhandled exception:
        expect(target.errorInPromise).not.toBeDefined();
        // this asks the spy if everything was as expected:
        //for (i = 0; i < haveBeenCalledWithArgs.length; i++) {
        //    stringedArgs += '\'' + haveBeenCalledWithArgs[i] + '\',';
        //}
        //stringedArgs = stringedArgs.substring(0, stringedArgs.length - 1);
        expect(promiseSpy).toHaveBeenCalledWith(haveBeenCalledWithArgs);
    });
},
SpyReturningPromise: function (target, methodName, callOnCallFake) {
    'use strict';
    var spyObj = spyOn(target, methodName).andCallFake(function () {
        spyObj.called = true;
        // http://stackoverflow.com/questions/13148356/how-to-properly-unit-test-jquerys-ajax-promises-using-jasmine-and-or-sinon
        var d = $.Deferred();
        callOnCallFake(d);
        return d.promise();
    });
    return spyObj;
}

// example calling it where calling myModule.dataService.UiDataService.GetComponentsAsync will resolve with an empty array when called
getComponentsPromiseSpy = SpecHelper.SpyReturningPromise(
    myModule.dataService.UiDataService,
    "GetComponentsAsync",
    function (deferred) {
        deferred.resolve([]);
    }
);
Here's a link to doing promises with AngularJs
  • To assert a single call to a ko.postbox.subscribe, when you have multiple calls:
    // in SpecHelper.js
    function WasPostboxSubscribeCalledForKey(key) {
        'use strict';
        var calls = ko.postbox.subscribe.calls,
            called = false,
            i;
        for (i = 0; i < calls.length; i++) {
            if (calls[i].args[0] === key) {
                called = true;
                break;
            }
        }
        return called;
    }
    
    expect(WasPostboxSubscribeCalledForKey('yourSubscribeKey')).toBeTruthy();
    expect(called).toBeTruthy();
  • use the Jasmine-jquery library to help test jQuery. I like to wrap these calls, which allows that call to be mocked, instead of having to setup ‘fixtures’.
  • Create custom matchers to help.
  • Use the mostRecentCall to test toHaveBeenCalledWith specific parameters if you want to use a regular expression match, or a more custom match.
    it("should initialize the grid with scrollMode auto", function () {
        // Arrange
        var gridElementId = '#cmp_1014 > .grid > table',
            gridElement = $(gridElementId);
        // Act
        vm.Init([], function () { }, function () { }, gridElementId, 10, 10);
        // Assert
        expect(dashboards.WijmoUtils.ChangeGridScrollMode.mostRecentCall.args[0].selector).toBe(gridElementId);
        expect(dashboards.WijmoUtils.ChangeGridScrollMode.mostRecentCall.args[1]).toBe('auto');
    });
  • Some spots in the code, you’ll just need to skip for tests. This may be more of a hack, and point to bad design, but I’ve found setting window.IsUnitTesting = true; at the top of the spec JavaScript file, then wrapping areas in if(!window.IsUnitTesting) does the job in a pinch.
  • spyOn jQuery to override calls: (don't forget to look at the Jasmine-jquery library that is linked above)
    // Emulates the JQuery Object and uses the callFake instead of $(‘#id’).width()/height();
    spyOn($.prototype, 'height').andCallFake(function () {
         return windowHeight;
    });
    spyOn($.prototype, 'width').andCallFake(function () {
        return windowWidth;
     });
  • expect a jQuery click event was setup:
  •  it("should setup click event listener on container", function () {
        // Arrange
        var clickSpy = spyOn($.prototype, 'click');
        // Act
        obj.AfterInit(pnl.ParentProperties, obj.Properties, obj);
        // Assert
        expect(clickSpy).toHaveBeenCalled();
    });
  • To reset a spy use the originalValue
    ko.postbox.subscribe = ko.postbox.subscribe.originalValue;
    found in the jasmine.js jasmine.Spec.prototype.removeAllSpies method
  • Cheat sheet reference
Posted on Friday, April 5, 2013 3:18 PM Unit Testing , BDD , JavaScript , Jasmine | Back to top


Comments on this post: Jasmine Specification Testing Pointers

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Aligned | Powered by: GeeksWithBlogs.net