Creating JavaScript unit tests for Sugar 7.6.0

This tutorial will cover the creation of new Jasmine unit tests for testing your Sugar 7 front end code.
In order to follow this tutorial, you will need access to the Sugar 7 Unit Test repository.  Make sure you have the latest code.  If you do not have access, then request access here.  You must be a current SugarCRM Customer or Partner.

The key concepts for testing your Sugar 7 JavaScript code will be the same no matter the framework in use.

Testing a Dashlet

In order for this to be a realistic example, we need to identify a Sidecar component that we want to test.  You could write your test against any part of the Sugar application including the out of the box Sugar 7 Sidecar components but lets take a moderately complex dashlet that we introduced in a previous blog post called Creating a Dashlet for Sugar 7 List Views.

The Case Counts by Status dashlet that was designed to be installed on Sugar 7 Contacts, Accounts, or Cases List Views.  It queries Sugar and shows a quick summary of the number of Cases in each status.



For convenience, here is a direct link to the Case Counts by Status controller code.

We will be writing Jasmine tests for this custom dashlet.  So follow the steps in Creating a Dashlet for Sugar 7 List Views to install the custom dashlet into your Sugar custom folder in order to follow along.  If you already have a dashlet or view you want to test instead then you will need to tweak the examples below to match.

Creating our first Jasmine test

For detailed documentation on writing Jasmine tests, please refer to the Jasmine documentation. In this section, we are going to assume you are familiar with some of the Jasmine basics, such as working with specs, expectations, and file structure.

If you follow the steps to deploy the Sugar 7 unit tests (for example, here are the steps for installing unit tests in Sugar 7.6), you will have a test/ directory that is added to your Sugar 7 installation that includes many test files.  You will have Grunt and Karma installed too.



By convention, the tests/ directory mirrors the file structure of the rest of the Sugar application.

This means that when creating tests for our Case Count by Status dashlet, we will be creating them within a new file under the tests/custom/ directory.

To try our basic Sidecar test example, create the file below at tests/custom/clients/base/views/case-count-by-status/case-count-by-status.js.

case-count-by-status.js

/*
* Basic Jasmine tests for Case Count by Status dashlet
*/
ddescribe("Case Count by Status", function () {
/*
     * Some useful constants for our tests.
     * We use them to keep track of the module, layout, and view we are testing
*/
var moduleName = 'Cases';
var viewName = 'case-count-by-status';
var layoutName = 'record-list';

/*
     * Variables shared by all tests
*/
var app;
var view;
var layout;

/**
     * Called before each test below.  We use this function to setup (or mock up) the necessary pieces
     * in order to test our Sidecar controller properly.
     *
     * Typically, we need to define Sugar view metadata and ensure that our controller JS file has been loaded
     * by the Sidecar framework.  We utilize some SugarTest utility functions to accomplish this.
*/
beforeEach(function() {
// Proxy for our typical Sidecar `app` object
        app = SugarTest.app;
//Ensure test metadata is initialized
SugarTest.testMetadata.init();
//Load custom Handlebars template (usually optional)
SugarTest.loadCustomHandlebarsTemplate(viewName, 'view', 'base');
//Load custom component JS (required)
SugarTest.loadCustomComponent('base', 'view', viewName);
//Mock view metadata for our custom view (usually required)
SugarTest.testMetadata.addViewDefinition(
            viewName,
            {
'panels': [
                    {
                        fields: []
                    }
                ]
            },
            moduleName
        );
//Commit custom metadata into Sidecar
SugarTest.testMetadata.set();

//Mock the Sidecar context object
var context = app.context.getContext();
context.set({
            module: moduleName,
            layout: layoutName
        });
context.prepare();

//Create parent layout for our view using fake context
        layout = app.view.createLayout({
            name: layoutName,
            context: context
        });

//Create our custom View before each test
        view = app.view.createView({
            name : viewName,
            context : context,
            module : moduleName,
            layout: layout,
            platform: 'base'
        });
    });

/**
     * Perform cleanup after each test.
*/
afterEach(function() {
//Delete test metadata
SugarTest.testMetadata.dispose();
//Delete list of declared components
app.view.reset();
//Dispose of our view
view.dispose();
    });

/**
     * Make sure that our view object exists
*/
it('should exist.', function() {
expect(view).toBeTruthy();
    });

/**
     * Make sure that when our controller creates expected HTML when render is called
*/
it('should render HTML', function(){
view.render();
expect(view.$el.html()).toContain("LBL_CASE_COUNT_BY_STATUS_TOTAL");
    });

/**
     * Tests for the _parseModels function.
*/
describe('_parseModels', function(){

it('should parse values based on available models', function(){

// Mock out the list of models (this normally comes from API)
var models = [
new Backbone.Model({id:'1', status:'Open'}),
new Backbone.Model({id:'2', status:'Closed'}),
new Backbone.Model({id:'3', status:'Open'})
            ];
// Call function with our mock list
view._parseModels(models, false);

//We had 2 open Cases in models array
expect(view.values['Open'].count).toBe(2);
//We had 1 closed Case in models array
expect(view.values['Closed'].count).toBe(1);
//Only 2 Statuses should exist in values array
expect(_.size(view.values)).toBe(2);

//Expect to find 3 Cases
expect(view.totalCases).toBe(3);
        });

it('should handle empty models array', function(){

// No models
var models = [];
view._parseModels(models, false);
expect(_.size(view.values)).toBe(0);
expect(view.totalCases).toBe(0);

        });

it('should handle unexpected values gracefully (null)', function(){

// null models array
var models = null;
view._parseModels(models, false);
expect(_.size(view.values)).toBe(0);
expect(view.totalCases).toBe(0);

         });

it('should handle unexpected values gracefully (object)', function(){

// object instead of array
var models = {};
view._parseModels(models, false);
expect(_.size(view.values)).toBe(0);
expect(view.totalCases).toBe(0);
        });

    });
});


You can then run these basic tests using the grunt karma:dev command from the root of your Sugar installation.  This should launch Chrome and after a few moments you will see output in your console similar to below.

mmarum$ grunt karma:dev

Running "karma:dev" (karma) task

WARN [watcher]: Pattern "/Users/mmarum/Sites/SugarEnt-Full-7.6.0.0/custom/modules/**/clients/**/*.hbs" does not match any file.

INFO [karma]: Karma v0.12.37 server started at http://localhost:9876/

INFO [launcher]: Starting browser Chrome

Chrome 45.0.2454 (Mac OS X 10.9.5) LOG: 'INFO[2015-9-7 20:8:16]: Router Started'

Chrome 45.0.2454 (Mac OS X 10.9.5): Executed 6 of 2791 (skipped 2785) SUCCESS (0.549 secs / 0.13 secs)

With Karma running in dev mode, every time you modify the test file then the unit tests will re-run interactively.  Right now, this unit test suite verifies that the dashlet parses new sets of models correctly.  However, we haven't written any tests that verifies what happens when you remove models.

As an exercise, try adding more tests to our example test suite that ensures that the Case Count by Status dashlet properly parses removal of models as well.

Anatomy of a Sugar 7 Jasmine Test Suite

You will notice that our test has some interesting features.

SugarTest helpers in beforeEach() function

The beforeEach() function contains several lines of code that will be similar for all your Sidecar Jasmine unit tests.  This is because it is used to setup your view's dependencies (like metadata, templates, context, and loaded JS code) in order to properly scaffold your custom View so that it can be created and tested properly.  In the main Sugar 7 application this plumbing is handled for you automatically.  But in order to properly isolate and unit test your JavaScript code, you need to set the plumping up manually using a variety of SugarTest helper functions.

In beforeEach(), many of the SugarTest helpers assume that you are only testing base Sugar 7 application files, so you will find specific utilities for working with JavaScript files and Handlebars templates located under custom/ folder within custom-component-helper.js.

Cleanup in afterEach() function

It is important to remember to cleanup after ourselves after each test.  This is necessary in order to ensure that our tests don't interact with each other during execution and to prevent memory leaks that could harm performance of your test run.

In afterEach(), you should clear out custom metadata that you've used as well as dispose of any views or layouts you created during your test.

ddescribe() versus describe() functions

By default, the Karma test runner will run all the available unit tests.  This can take a couple minutes as there are thousands of tests to run that are part of our base Sugar 7 test suite.  So adding an extra 'd' in front of our describe() function at the top of file is a clue to Karma to run these tests exclusively as a convenience.  This blog post explains more about how to use exclusive tests.  Just remember to change function name back to describe() when you are ready to commit your test into the full test suite.

Assertions are within it() functions

The actual testing happens within Jasmine's it() functions.  We only have a handful of assertions where we use Jasmine's expect() function in this example.  But clearly, you can add as many assertions as you want once you have your Sidecar view setup properly.

Jasmine Test Template

As a convenience, here is a handy template for creating your own Jasmine tests for Sugar 7 views.  Just look for the TODOs within and update those sections appropriately based on the view you are testing.  The trickiest part of writing tests is to making sure your view's dependencies are setup appropriately in the beforeEach() function.  But once that's complete, you'll find creating actual Jasmine specs a snap!

test-template.js

/*
* Basic Jasmine test template for any Sugar 7 view
*/
ddescribe("Jasmine template for Sugar 7 views", function () {
/*
     * Some useful constants for our tests.
     * We use them to keep track of the module, layout, and view we are testing
*/
var moduleName = 'Accounts';    //TODO CHANGE TO AN APPROPRIATE MODULE
var viewName = "CHANGE_ME";     //TODO CHANGE TO YOUR VIEW NAME
var layoutName = "record-list"; //TODO CHANGE TO YOUR PARENT LAYOUT NAME

/*
     * Variables shared by all tests
*/
var app;
var view;
var layout;

/**
     * Called before each test below.  We use this function to setup (or mock up) the necessary pieces
     * in order to test our Sidecar controller properly.
     *
     * Typically, we need to define Sugar view metadata and ensure that our controller JS file has been loaded
     * by the Sidecar framework.  We utilize some SugarTest utility functions to accomplish this.
*/
beforeEach(function() {
// Proxy for our typical Sidecar `app` object
        app = SugarTest.app;
//Ensure test metadata is initialized
SugarTest.testMetadata.init();

/**
         * TODO LOAD ANY ADDITIONAL DEPENDENCIES USING SugarTest.load FUNCTIONS HERE
*/

//Load custom Handlebars template
SugarTest.loadCustomHandlebarsTemplate(viewName, 'view', 'base' /*, moduleName */);
//Load custom component JS
SugarTest.loadCustomComponent('base', 'view', viewName /*, moduleName */);


//Mock view metadata for our custom view
SugarTest.testMetadata.addViewDefinition(
            viewName,
//TODO SETUP YOUR FAKE VIEW METADATA HERE
            {
'panels': [
                    {
                        fields: []
                    }
                ]
            },
            moduleName
        );
//Commit custom metadata into Sidecar
SugarTest.testMetadata.set();

//Mock the Sidecar context object
var context = app.context.getContext();
context.set({
            module: moduleName,
            layout: layoutName
        });
context.prepare();

//Create parent layout for our view using fake context
        layout = app.view.createLayout({
            name: layoutName,
            context: context
        });

//Create our View before each test

        view = app.view.createView({
            name : viewName,
            context : context,
            module : moduleName,
            layout: layout,
            platform: 'base'
        });

    });

/**
     * Perform cleanup after each test.
*/
afterEach(function() {
//Delete test metadata
SugarTest.testMetadata.dispose();
//Delete list of declared components
app.view.reset();
//Dispose of our view
view.dispose();
    });


/**
     * Make sure that our view object exists
*/
it('should exist.', function() {
expect(view).toBeTruthy();
    });

/**
     * TODO ADD YOUR TESTS HERE WITHIN DESCRIBE() AND IT() FUNCTIONS
*/

});