Unit testing $modal with Jasmine
Asked Answered
P

2

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.

Platitude answered 27/10, 2014 at 18:24 Comment(0)
J
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();
    });
});
Jethro answered 27/10, 2014 at 18:53 Comment(13)
I was mid my answer when you posted it, you nailed it ;)Fishmonger
@Fishmonger haha it happens sometimes to me as well.. :)Jethro
@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.Platitude
@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.Jethro
@TheDIMMReaper i got it, in my example i removed beforeEach(module('app')) add this and you should be goodJethro
@Jethro - I noticed that, too, and tried adding it before I responded. But, that only changes the error to object is not a function.Platitude
@TheDIMMReaper which line is throwing object is not a function?Jethro
@Jethro - 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 showing the latest error.Platitude
@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 plnkr.co/edit/C10qMz?p=preview and for your version of jasmine jsfiddle.net/wovbzgr1Jethro
@Jethro - 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!Platitude
Thanks @Jethro 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 charmAlannaalano
How would this be managed if testing a directive? If there was on controller you could inject the mock into?Alannaalano
@Alannaalano Use $provide to override $modal service with mock.Jethro
R
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.

Rriocard answered 27/10, 2014 at 18:48 Comment(2)
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.Jethro
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!Platitude

© 2022 - 2024 — McMap. All rights reserved.