6

I have an Angular app with a controller which displays an Angular-Strap modal window during a function call. It functions correctly in Chrome, but I am at a loss getting a valid unit test working.

App module and the FooController:

var app = angular.module("app", ["mgcrea.ngStrap"]);

app.controller("FooController", function($scope, $modal) {
    var fooModal = $modal({
        title: 'Foo',
        content:'Bar',
        show: false,
        html: true,
        backdrop: 'static',
        placement: 'center'});
    
    angular.extend($scope, {
        makeItFoo: function() {
            fooModal.show();
        }
    });
});

Controller spec:

describe('FooController', function () {
    var scope, controller, modal;

    beforeEach(module('app', function ($provide) {
        // Stub out $modal service
        $provide.value('$modal', function () {
            return {
                hide: function () { },
                show: function () { }
            };
        });
    }));

    beforeEach(inject(function ($rootScope, $controller, $injector) {
        //set up a new scope and the controller for the test
        scope = $rootScope.$new();
        controller = $controller('FooController', {$scope: scope});
        modal = $injector.get('$modal');
    }));

    it('should show the modal', function () {
        var modalSpy = spyOn(modal(), 'show');
        
        scope.makeItFoo();
        
        expect(modalSpy).toHaveBeenCalled();
    });
});

Here's a fiddle as well.

I expect my call to makeItFoo() to display the modal, but Jasmine fails the test with the error Expected spy show to have been called. I've also tried setting the show property of the modal to true and not calling show() separately, and I've tried other variants of stubbing the $modal service and injecting it directly into the controller, but it ends up with the same error.

I'm using AngularJS 1.2.14, Angular-Strap 2.0.0, and Jasmine 1.3.1.

Community
  • 1
  • 1
The DIMM Reaper
  • 3,558
  • 3
  • 28
  • 46

2 Answers2

7

Instead of doing these. Create a mock object for $modal with show and hide methods and set your expectations on them.

describe('FooController', function () {
    var scope, controller, modal;

    beforeEach(module('app'));

    beforeEach(inject(function ($rootScope, $controller) {
        //set up a new scope and the controller for the test
        scope = $rootScope.$new();
        //Create spy object
        modal = jasmine.createSpyObj('modal', ['show', 'hide']);
        //provide modal as dependency to the controller.
        controller = $controller('FooController', {$scope: scope, $modal:modal});

    }));

    it('should show the modal', function () {

        scope.makeItFoo();

        expect(modal.show).toHaveBeenCalled();
    });
});
PSL
  • 123,204
  • 21
  • 253
  • 243
  • 1
    I was mid my answer when you posted it, you nailed it ;) – maurycy Oct 27 '14 at 18:54
  • @maurycy haha it happens sometimes to me as well.. :) – PSL Oct 27 '14 at 18:55
  • @PSL, thanks! I was trying to understand the correct way to mock $modal, and this appears to be it. However, having updated my fiddle as you described, the test is still failing. Specifically, the error now reads `Argument 'FooController' is not a function, got undefined`; updated fiddle [here](http://jsfiddle.net/dimmreaper/jwom7ns2/3/). – The DIMM Reaper Oct 27 '14 at 20:16
  • @TheDIMMReaper I cannot access fiddle here, i use mocks every day, it must be some other issue. do you by any chance put it in a working jsbin or i will have to get back home to check the fiddle. – PSL Oct 27 '14 at 20:27
  • @TheDIMMReaper i got it, in my example i removed `beforeEach(module('app'))` add this and you should be good – PSL Oct 27 '14 at 20:33
  • @PSL - I noticed that, too, and tried adding it before I responded. But, that only changes the error to `object is not a function`. – The DIMM Reaper Oct 27 '14 at 20:51
  • @TheDIMMReaper which line is throwing object is not a function? – PSL Oct 27 '14 at 20:53
  • 1
    @PSL - I'm not sure how to interpret the line numbers in the Jasmine output in JSFiddle. I'm trying to replicate this (much) simplified example back into my IDE so that I can understand it better. I'll reply again once it is working there. In the meantime, [here's an updated fiddle](http://jsfiddle.net/dimmreaper/jwom7ns2/4/) showing the latest error. – The DIMM Reaper Oct 27 '14 at 22:07
  • 3
    @TheDIMMReaper Here you go. I have not used this ng-strap, it seems like it provides a constructor instead of instance. Here is how you can write test for this http://plnkr.co/edit/C10qMz?p=preview and for your version of jasmine http://jsfiddle.net/wovbzgr1/ – PSL Oct 28 '14 at 00:50
  • 1
    @PSL - you hit the nail on the head - good thinking making a spy for the constructor function and a second spy object for its return. That's exactly what threw me off in the first place. Thanks! – The DIMM Reaper Oct 28 '14 at 13:58
  • Thanks @PSL you have saved me a headache as I was able to inject the $modal dependency when i was testing directives that got compiled but because this does not get compiled the controller didn't recognise the injected dependency, so your approach to mocking the modal worked like a charm – user1005240 May 19 '15 at 11:08
  • How would this be managed if testing a directive? If there was on controller you could inject the mock into? – user1005240 Jun 11 '15 at 14:22
  • @user1005240 Use $provide to override $modal service with mock. – PSL Jun 11 '15 at 14:28
1

The modal show is async. I updated your fiddle at http://jsfiddle.net/jwom7ns2/1/.

Change the following portion:

it('should show the modal', function (done) {
    var modalSpy = spyOn(modal(), 'show');

    scope.makeItFoo();

    setTimeout(function() {
        expect(modalSpy).toHaveBeenCalled();
        done();
    });

});

The timeout wrapper waits for the digest to happen when the modal show occurs.

rtucker88
  • 986
  • 8
  • 15
  • I was about to comment before you revised your answer on why `scope.$apply`?. But i think this as well probably is an overkill IMHO, because OP already created a mock obj and then setting up spy. Instead all can be done at once by creating a mock object using jasmine. Also you really do not need to do `$injector.get('$modal')` instead `$modal` can be directly injected. – PSL Oct 27 '14 at 18:56
  • 1
    Thanks, this definitely seems like a viable solution. I'm going to give a shot first to the @PSL's method of mocking $modal, as I'd prefer not to deal with timing explicitly. If I have trouble there, I'll keep your solution in mind. Thanks! – The DIMM Reaper Oct 27 '14 at 20:19