2

I'm using Karma + Mocha to test a controller in Angular. The code below is a simplified example of the controller & test spec. L.Control.Locate is a LeafletJS plugin.

The problem

During a test run L.Control.Locate should exist when the controller is instantiated, but it doesn't. It throws: TypeError: L.Control.Locate is not a constructor.

The app works as expected in the browser.

The error occurs in both PhantomJS and Chrome.

In the test I've confirmed with a debugger that before MapCtrl2 is instantianted, L.Control.Locate is declared and attached to window.L.Control as it should be, but it's getting lost before the controller is instantiated.

The other standard properties for L are there as expected.

When I paste L.Control.Locate.js into the file before the controller declaration, the error disappears.

I've also tried injecting $window into the controller and looking at $window.L.Control.Locate, but it's still undefined.

Thanks for any clues.

Code:

angular.module('app')
.controller('MapCtrl2', function ($scope) {

  // Throws error here.
  var locateControl = new L.Control.Locate();

  // Do something with locateControl.

});

describe('MapCtrl2', function () {

  var controller, scope;
  beforeEach(module('app'));
  beforeEach(inject(function ($controller, $rootScope) {
    scope = $rootScope.$new();
    controller = $controller('MapCtrl2', {
      $scope: scope
    });
  }));

  it('has L.Control.Locate', function() {
    expect(window.L.Control.Locate).to.be.an.instanceOf(Object)
  });

});

Update:

Regarding the issues around using L globally, I've realized L is available via $window.L. Though that was probably obvious to many AngularJS devs, I overlooked it.

kentr
  • 969
  • 1
  • 11
  • 21
  • Mock it. For mocking functions that don't return a value, angular.noop is your friend. – Amy Blankenship Mar 31 '16 at 22:19
  • @AmyBlankenship: If I understand mocking correctly, I don't think that's what I want. I believe I want the full functionality. Either way, I'd still like to understand why it's not available in the context. – kentr Mar 31 '16 at 22:42
  • 1
    No, you don't. The whole point of tests is that you are testing the one little bit of code you are working on, not some other code you don't even own. The reason it's not available is because your test runner isn't loading the library. And you shouldn't ask it to. – Amy Blankenship Mar 31 '16 at 22:43
  • Sure, but the goal isn't to test the code I don't own, it's to test how my code interacts with the library. If I'm mocking it, seems that I'd have to mock that library's interface & functionality. Is that the approach? – kentr Mar 31 '16 at 22:54
  • Yes. There may be libraries that automate that, given you can provide them with a sample of the object to mock. But because this thing is just attached to the window object it makes it difficult to replace. That's one reason frameworks like Angular exist, because sticking junk on the window object is just nasty. – Amy Blankenship Mar 31 '16 at 23:00

1 Answers1

1

To mock your library object, you want something like

beforeEach(
    function() {
        window.L = {
            Control: {
                Locate: function() {
                    self = this;
                    //populate based on the API you're mocking
                    self.someMethod = angular.noop;
                    self.someProp = 'foo';
                }
            }
        }
    }
}

Then you want to spy on the methods to make sure your controller is calling them as expected, or read the properties, to make sure your controller is setting them as expected.

Note I inherited code that attaches itself to the window object (yuk!). I hide that dependency as much as possible. I would never let controllers touch it directly.

Amy Blankenship
  • 6,485
  • 2
  • 22
  • 45
  • Thx. I mostly get how to mock, but I'm apparently missing some fundamental concepts of Angular and this kind of testing. Two cases come to mind: 1) I want to test the result of using this interface on some of my data (from my service, for example). For that, wouldn't I need a functional object instead of a mock? 2) I want to know when my code breaks b/c of a change in the external library. Is it reasonable to test for these, and if so, wouldn't I need functional code instead of just a noop? – kentr Apr 01 '16 at 00:19
  • If you need to test the result of getting a specific value as the result of your function call, then the function call needs to return that value. You can do that directly in the mock object, or you can spyOn the method and.returnValue() or and.callFake() depending on the nature of the test. I know that sometimes you're forced to use libraries from people who don't write tests, but your unit tests should only be testing your own code. Their tests should test theirs. – Amy Blankenship Apr 01 '16 at 15:51
  • Sorry, that the and.returnValue, etc. is jasmine syntax, but I'm sure mocha has something similar. – Amy Blankenship Apr 01 '16 at 15:56
  • 1
    This answer worked for my situation. After realizing I can get `L` through `$window`, I may refactor. – kentr Apr 02 '16 at 04:42