How to mock ng-grid when unit testing a controller

All we need is an easy explanation of the problem, so here it is.

I’m currently trying to write tests for existing blocks of code and running into an issue with a controller that has a nested ng-grid inside of it. The issue comes from the controller trying to interact with the grid on initialization.

Testing Software
[email protected]
[email protected]
[email protected]
[email protected]

My Test:

define(["angularjs", "angular-mocks", "jquery",
    "js/3.0/report.app",
    "js/3.0/report.controller",
    "js/3.0/report.columns"
],
    function(angular, ngMocks, jquery, oARModule, oARCtrl, ARColumns) {
        "use strict";

        describe("Report Center Unit Tests", function() {
            var oModule;

            beforeEach(function() {
                oModule = angular.module("advertiser_report");
                module("advertiser_report");
            });

            it("Advertiser Report Module should be registered", function() {
                expect(oModule).not.toBeNull();
            });

            describe("Advertiser Report Controller", function() {
                var oCtrl, scope;

                beforeEach(inject(function($rootScope, $controller, $compile) {
                    var el = document.createElement('div');
                     el.setAttribute('ng-grid','gridOptions');
                     el.className = 'gridStyle';
                    scope = $rootScope.$new();
                    $compile(el)(scope);
                    oCtrl = $controller('ARController', {
                        $scope: scope
                    });
                }));

                it("Advertiser Report controller should be registered", function() {
                    expect(oCtrl).not.toBeNull();
                });
            });
        });
    });

You’ll see where I’ve tried to create and compile an element with the ng-grid attribute. Without doing this I get the following error:

TypeError: Cannot read property ‘columns’ of undefined

Which is a result of the controller attempting to call things like
$scope.gridOptions.$gridScope.columns.each

So I added the creation of a div with ng-grid attribute, and got a new error:

TypeError: Cannot set property ‘gridDim’ of undefined

So, I tried to add scope.gridOptions before the $controller call, but this brought me back to the original error. I’ve been searching for way to make this work without rewriting the controller and/or templates, since they are currently working correctly in production.

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Your (major!) problem here is that the controller is making assumptions about a View. It should not know about and thus not interact with ng-grid. Controllers should be View-independent! That quality (and Dependency Injection) is what makes controllers highly testable. The controller should only change the ViewModel (i.e. its $scope), and in testing you validate that the ViewModel is correct.

Doing otherwise goes against the MVVM paradigm and best practices.

If you feel like you must access the View (i.e. directives, DOM elements, etc…) from the controller, you are likely doing something wrong.

Method 2

The problem in the second Failing test is gridOptions and myData is not defined prior to the compilation. Notice the sequence of the 2 statements.

Passing
oCtrl = $controller(‘MainCtrl’, { $scope: $scope });
$compile(elm)($scope);

Failing

$compile(elm)($scope);
oCtrl = $controller('MainCtrl', { $scope: $scope });

In both cases you are trying to use the same html

elm = angular.element('<div ng-grid="gridOptions" style="width: 1000px; height: 1000px"></div>');

I suggest you get rid of

oCtrl = $controller('MainCtrl', { $scope: $scope });

maneuvers and use the following HTML element instead

elm = angular.element('<div ng-controller="MainCtrl" 
      ng-grid="gridOptions" style="width: 1000px; height: 1000px"></div>');

Notice ng-controller="MainCtrl".

So the end story is that you need gridOptions defined somewhere so
that it ngGrid can access it. And make sure gridOptions dependent
code in controller is deferred in a $timeout.

Also take a look at the slight changes in app.js

$timeout(function(){
    //your gridOptions dependent code
    $scope.gridOptions.$gridScope.columns.each(function(){
      return;
    });  
  });

Here is the working plnkr.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply