SharePoint, RequireJS and Jasmine Tests

I Want TDD for SharePoint!

Well, I'm back. I haven't been really happy with how little testing I do with a lot of my SharePoint Solutions. It's just habit, I guess, and it seems that that this is super common in the SP development world.

I do most of my development at the collection level using either the JavaScript CSOM or REST endpoints, with a preference for REST (just because I feel like it has more of a connection to the rest of the web development world). In order to get to a TDD flow, I needed to create some way to integrate testing and to simplify module loading.

RequireJS and Jasmine are 2 great libraries that address these issues. I'm going to walk through how to set these up for use in SharePoint and show a couple of non-trivial tests to get you started.

This is a solution that anyone with Collection Admin rights can use. It does not require Apps (though it could be easily ported to that model) to be set up, nor does it require anything to be done on the server.

Structure

I am going to assume that these modules will be most useful when paired with custom layouts and/or master pages. Here's a nice way to organize the _catalogs/masterpage with individual layouts following a standard app structure:

+-- .../_catalogs/masterpage             // Root Folder
|   +-- custom                           // Custom Master / Layouts Folder
|   |   +-- custom-master.html           // Custom Master Page HTML there could be multiple
|   |   +-- custom-master.master         // Custom Master Page - auto created from HTML
|   |   +-- Styles                       // Styles Folder for styles used throughout Custom
|   |   |   +-- custom-master.css        // css for custom master
|   |   |   +-- jasmine-styles           // Style the results
|   |   |   |   +-- jasmine.css          // Jasmine css
|   |   +-- Scripts                      // Scripts Folder for scripts used throughout Custom
|   |   |   +-- lib                      // Library for js scripts
|   |   |   |   +-- jquery.min.js        // Standard name for jquery to prevent renaming tags
|   |   |   |   +-- jquery-nc.js         // Returns $.noConflict()
|   |   |   |   +-- jasmine              // Jasmine library
|   |   |   |   |   +-- jasmine.js       // Jasmine
|   |   |   |   |   +-- jasmine-html.js  // Jasmine-html
|   |   |   |   |   +-- mock-ajax.js     // Mock the calls
|   |   |   |   |   +-- MIT.LICENSE      // Yay!
|   |   |   |   +-- require              // RequireJS
|   |   |   |   |   +-- require.min.js   // RequireJS
|   |   |   |   |   +-- domReady.js      // RequireJS domReady plugin
|   |   |   |   |   +-- text.js          // RequireJS text plugin
|   |   +-- test-layout                  // Layout specific app - named for what this layout does
|   |   |   +-- test-layout.html         // Custom Layout HTML one layout per app
|   |   |   +-- test-layout.aspx         // Custom Layout - auto generated from HTML
|   |   |   +-- app                      // Folder for app assets
|   |   |   |   +-- config-require.js    // Config file for RequireJS
|   |   |   |   +-- css                  // Folder with css styles for layout
|   |   |   |   |   +-- test-layout.css  // Custom Layout css
|   |   |   |   +-- modules              // JS modules used by app
|   |   |   |   |   +-- RESTUtils.js     // Module to make REST Calls
|   |   |   |   |   +-- start.js         // Init
|   |   |   +-- tests                    // Where Jasmine Tests Live - can call it specs too
|   |   |   |   +-- spec-runner          // Folder for jasmine spec runner
|   |   |   |   |   +-- spec-runner.aspx // spec-runner
|   |   |   |   +-- modules              // Folder for module unit tests
|   |   |   |   |   +-- specRESTUtils.js // Test RESTUtils

Obviously things like jQuery, RequireJS, Jasmine and their respective plugins need to be downloaded and placed into the folder structure.

Jasmine Results Page

Now I could have just copied the Jasmine Runner as an HTML page, but that won't allow me to tap into the SharePoint Context variables (which I use in a lot of code).

First, thanks to Thorsten Hans for giving me a starting point for all of this.

The spec-runner.aspx page is super minimal. It registers a couple of tag prefixes, adds the Jasmine style sheet and pulls in the RequireJS configuration. The entirety of it's code is:

<%@Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>  
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>  
<%@Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>

<asp:Content ContentPlaceHolderId="PlaceHolderAdditionalPageHead" runat="server">  
    <SharePoint:ScriptLink name="sp.js" runat="server" OnDemand="true" LoadAfterUI="true" Localizable="false" />
    <link rel="stylesheet" type="text/css" href="../../../Styles/jasmine-styles/jasmine.css" ms-design-css-conversion="no" />
    <script type="text/javascript" data-main="../../../client-pages/app/config-require" src="../../../Scripts/lib/require.min.js"></script>
</asp:Content>

<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">  
    <div id="jasmine-specs"></div>
</asp:Content>  

Depending on the modules I am using, there may be reason to register some other tag prefixes, but this is a great start.

Configuring RequireJS

I like this set up a lot. With it I can plug in any scripts I need without touching the HTML, this is unspeakably valuable with modular development. I would also point out here, that I am only showing the Testing setup. I have a separate configuration and start file for production that leaves out the test scripts.

Anyway the configure-require.js file is structured like this:

define(function () {  
    var rconfig = {
        // relative url to this site collection's masterpage/custom folder
        baseUrl: '/sites/<COLLECTION>/_catalogs/masterpage/custom/',
        // dependencies
        deps: ['spruntime_js', 'sp_js'],
        paths: { ... },
        map: { ... },
        shim: { ... }
    };
    // send configuration to RequireJS
    requirejs.config(rconfig);
    // run require to load modules
    requirejs(['...'], function () {});
});

One very important note, RequireJS does not want the .js file extension, that is assumed. If you put the extension in it will not find the file.

There needs to be a path to everything that is being used in this project, so I fill that in with:

 paths: {
    // SharePoint Runtime & Core
    // Register any SharePoint Library if it is required to be processed first
    'spruntime_js'  : window.location.origin + '/_layouts/15/sp.runtime',
    'sp_js'         : window.location.origin + '/_layouts/15/sp',

    //RequireJS plugins
    'domReady'      : 'Scripts/lib/require/domReady',     // to make sure DOM is loaded
    'text'          : 'Scripts/lib/require/text',         // to use html templates

    // Frameworks
    'jquery-nc'     : 'Scripts/lib/jquery-nc',
    'jquery'        : 'Scripts/lib/jquery.min',

    // Test Frameworks
    'jasmine'       : 'Scripts/lib/jasmine/jasmine',
    'jasmine-html'  : 'Scripts/lib/jasmine/jasmine-html',
    'jasmine-boot'  : 'Scripts/lib/jasmine/lib/boot',
    'mock-ajax'     : 'Scripts/lib/jasmine/mock-ajax',

    // Modules
    'boot'          : 'client-pages/app/modules/start',
    'restUtils'     : 'client-pages/app/modules/RESTUtils',

    // Test Modules
    'specRESTUtils' : 'client-pages/tests/modules/specRESTUtils',
},

I use the map object to make jQuery run in noConflict and the shim object for files that don't follow AMD modular standards (looking at you Jasmine). Here are those 2 objects:

map: {  
    // give all modules jQuery no-conflict
    '*'             : { 'jquery': 'jquery-nc' },

    // prevent unresolved dependency on jquery for jquery-ncbase
    'jquery-nc'     : { 'jquery': 'jquery' }
},
shim: {  
    'jasmine-html'  : { deps: ['jasmine'] },
    'jasmine-boot'  : { deps: ['jasmine', 'jasmine-html'] },
}

Open the jasmine boot.js file and change the Execute block to:

/**
   * ## Execution
   *
   * No onload, only on demand now
   */

  window.executeTests = function(){
    htmlReporter.initialize();
    env.execute();
  };

Thanks to Eli Weinstock-Herman for this change, it will prevent a duplicate results bar from appearing on the spec-runner page.

Alright, now RequireJS knows where everything is, and I can see how simple it is to add new files/tests, but how do I get some thing to happen. Have requireJS load jasmine-boot, then mock-ajax, then the start module.

requirejs.config(rconfig);  
requirejs(['jasmine-boot'], function () {  
    requirejs(['mock-ajax'], function() {
        requirejs(['boot'], function () {
            // trigger Jasmine
            window.executeTests();
        });
    });
});

The Modules

Before I get too far ahead of myself, I mapped jQuery to a no-conflict state. The jquery-nc.js file is as simple as this:

define(['jquery'], function ($) {  
    return $.noConflict();
});

Now for some tests. Here's the contents for specRESTUtils.js:

var specRESTUtils = function (restUtils) {

    var _runSpecs = function() {

        describe("RESTUtils", function() {

            var url         =    _spPageContextInfo.webAbsoluteUrl + '/_api/web',
                headers     =   { 
                                    'accept': 'application/json;odata=verbose',
                                    'content-type': 'application/json;odata=verbose'
                                },
                body        =   { 'query' : {'__metadata': { 'type': 'SP.CamlQuery' }, "ViewXml": "<View></View>" } },
                request;

            // intercept ajax calls and fake them
            beforeEach(function() {
                jasmine.Ajax.install();
            });

            // stop intercepting after each test
            afterEach(function() {
                jasmine.Ajax.uninstall();
            });


            describe("getPromise", function() {
                it("Makes an ajax GET request to passed url", function() {
                    var getTest = jasmine.createSpy("success");

                    restUtils.Get(url, headers);

                    request = jasmine.Ajax.requests.mostRecent();

                    expect(request.url).toBe(url);
                    expect(request.method).toBe('GET');
                    expect(getTest).not.toHaveBeenCalled();
                });
            });

            describe("deletePromise", function() {
                it("Makes an ajax POST call with the DELETE verb", function() {
                    var delTest = jasmine.createSpy("success");

                    restUtils.Delete(url, headers, "*");

                    request = jasmine.Ajax.requests.mostRecent();

                    expect(request.url).toBe(url);
                    expect(request.method).toBe('POST');
                    expect(request.requestHeaders["X-HTTP-Method"]).toBe("DELETE");
                    expect(request.requestHeaders["IF-MATCH"]).toBe("*");
                    expect(delTest).not.toHaveBeenCalled();                        
                });
            });

            describe("postPromise", function() {
                it("Makes an ajax POST call with body text", function() {
                    var postTest = jasmine.createSpy("success");

                    restUtils.Post(url, headers, body);

                    request = jasmine.Ajax.requests.mostRecent();

                    expect(request.url).toBe(url);
                    expect(request.method).toBe('POST');
                    expect(request.params).toBe(JSON.stringify(body));
                });
            });
        });
    };
    return { runSpecs : _runSpecs }
};

define(['restUtils'], specRESTUtils);  

Using the RequireJS module format I defined the Jasmine tests for GET, POST & DELETE calls to my current web endpoints. Notice in the last statement there is a dependency for restUtils, whose path was defined in the config-require file.

It needs to test something tho, here's the contents of RESTUtils.js:

"use strict";

var restUtils = function ($) {

    function _getPromise(url, headers) {

        return _doCall(url, headers, "", "GET")
    }

    function _deletePromise(url, headers, eTag) {
        return _postPromiseInternal(url, headers, "", "DELETE", eTag);
    }

    function _postPromise(url, headers, body) {
        return _postPromiseInternal(url, headers, body);
    }

    function _postPromiseInternal(url, passedheaders, body, action, eTag) {
        //local variable to store headers passed to actual AJAX call
        var localHeaders = {};

        //Serialize the body so it can be passed to the AJAX call and also so we can set the Content-Length header
        var bodyString = JSON.stringify(body);

        //Get each passed header into localHeaders
        for (var key in passedheaders) {
          if (passedheaders.hasOwnProperty(key)) {
            localHeaders[key] = passedheaders[key];
          }
        }

        //Request Digest header
        localHeaders["X-RequestDigest"] = $("#__REQUESTDIGEST").val();

        //Content Length header
        if (body) {
            localHeaders["Content-Length"] = bodyString.length;
        }
        else {
            localHeaders["Content-Length"] = 0;
        }

        //Verb-tunneling for other verbs which may be blocked by firewalls
        if (action && action.length > 0) {
            localHeaders["X-HTTP-Method"] = action;

            //If-Match header, used passed value or default to *
            if (eTag && eTag.length > 0) {
                localHeaders["IF-MATCH"] = eTag;
            }
            else {
                localHeaders["IF-MATCH"] = "*";
            }
        }


        //Make the call
        return _doCall(url, localHeaders, bodyString, "POST")
    }

    function _doCall(url, passedheaders, bodyString, verb) {
        //Make sure we have an ACCEPT header, set it if not
        if (!passedheaders || !passedheaders.Accept || passedheaders.Accept.length === 0) {
            passedheaders["Accept"] = "application/json;odata=verbose";
        }

        var dfd = $.ajax({
            url: encodeURI(url),   //Make sure to encode the URI
            type: verb,
            contentType: "application/json;odata=verbose",
            data: bodyString,
            headers: passedheaders
        });

        //Everything returns a Promise
        return dfd.promise();
    }

    // Utility function to return a fulfilled promise, used only for testing and prototyping
    function _returnResolvedPromise() {
        var dfd = $.Deferred();
        dfd.resolve();
        return dfd.promise();
    }

    var publics = {
        Get: _getPromise,
        Post: _postPromise,
        Delete : _deletePromise,
        ReturnResolvedPromise: _returnResolvedPromise,
    }

    return publics;

};

define(['jquery'], restUtils);  

I picked up this really nice REST abstraction from somewhere I can't seem to find at the moment. It was in a REST vs JSOM solution and the developer used the namespace s43, if I find the link I will post it here.

Anyway, I tweaked it a little and used the RequireJS module format.

The last piece of this puzzle is the start.js file that calls the tests. Here it is:

var startModule = function ( $, specRest ) {

    function RunTests() {
        specRest.runSpecs();
    };    

    SP.SOD.executeFunc('sp.js', 'SP.ClientContext', RunTests);
};

// By waiting for the body to load, we ensure that all of the SharePoint Context variables are available
ExecuteOrDelayUntilBodyLoaded(function () {  
    define(['jquery','specRESTUtils'], startModule);
});

Now open the spec-runner in the browser and watch it go.

I am really happy with this set up, it gives a nice template that can be quickly duplicated and the RequireJS configuration makes life simple by