wardbell / bardjs

Spec helpers for testing angular v.1.x apps with Mocha, Jasmine and QUnit
MIT License
178 stars 34 forks source link

how to fake $window #1

Closed zardaloop closed 9 years ago

zardaloop commented 9 years ago

Hi Ward, I need help with bardjs and I would highly appreciate if you could help me.

I am using John Papa's gulp pattern (https://github.com/johnpapa/gulp-patterns) where he uses bardjs. I want to fake $window because I have some stuff stored in the sessionstorage which is used within the application. What would be the best way of adding additional fake providers?

I know that I can go ahead and add a file like this (https://github.com/johnpapa/ng-demos/blob/master/cc-bmean/src/client/test/lib/specHelper.js) and there define the fakeWindow within that file like this:

function fakeWindow($provide) {
        $provide.provider('$window', function () {
            /* jshint validthis:true */
            this.$window = sinon.stub();

            this.$get = function () {
                return {
                    sessionStorage: sinon.stub({
                        token: 'someToken'
                    })
                };
            };
        });
 }

and then for example in this file (https://github.com/johnpapa/gulp-patterns/blob/master/src/client/app/customers/customers.route.spec.js), instead of having :

beforeEach(function() {
        module('app.customers', bard.fakeToastr);
        bard.inject(this, '$location', '$rootScope', '$state', '$templateCache');
 });

do something like this:

beforeEach(function () {
        module('app.customers', function ($provide) {
            bard.fakeToastr($provide);
            specHelper.fakeWindow($provide);
        });
        bard.inject(this, '$location', '$rootScope', '$state', '$templateCache', '$window');
 });

obviously this approach will do exactly what I need, but it is getting drifted away from bardjs.

So my question is how would you do it ?

Many thanks in advance.

Farzan

wardbell commented 9 years ago

You didn't say why you want to stub out $window but I can see in your code that you want to fake one of its properties, sessionStorage.

As you may know, $window is a kind of proxy for the browser window object. You write your application code to reference $window instead of window so that it is easier to test the parts of your application that need access to browser window features.

$window has the simplest possible implementation. It simply returns the browser's window object:

function $WindowProvider() {
    this.$get = valueFn(window); // valueFn returns a function that returns the window object
}

Now window is the biggest object in all of JavaScript. There is no way bard (or anyone) can anticipate everything that anyone might want to fake about it. We're not going to extend bard for $window mocking.

But it's really easy for you to fake exactly what you want ... by replacing Angular's $window implementation with your own.

Let's take what I believe to be your case. I think you want to fake behaviors of the window.sessionStorage object. Start by creating a fake sessionStorage that does what your test(s) need. I don't know what that is exactly but it might be:

var storedItem = ...; // whatever you want it to be
var fakeSessionStorage = {
    getItem: sinon.stub().returns(storedItem),
    setItem: sinon.spy(function setItem(key, obj) {storedItem = obj;})
}

Now create a fake window object that supports only what your tested component needs.

var fakeWindow = {
    sessionStorage: fakeSessionStorage
};

Now register your fake window during test module setup:

bard.module('app.customers', function ($provide) {
    $provide.value('$window', fakeWindow); // replaces ng's $window w/ the fake
});

Notice that I'm using bard.module which fakes toastr for you automatically.

Putting it all together we get:

beforeEach(function() {
    var storedItem = ...; // whatever you want it to be initially
    var fakeSessionStorage = {
        getItem: sinon.stub().returns(storedItem),
        setItem: sinon.spy(function setItem(key, obj) {storedItem = obj;})
    }

    bard.module('app.customers',  function ($provide) {
        $provide.value('$window', fakeWindow); // replaces ng's $window w/ the fake
    });
    bard.inject(this, '$location', '$rootScope', '$state', '$templateCache', '$window');
});

If you find yourself faking sessionStorage in multiple test files, you'll want to pull that effort into a spec helper function ... perhaps like this:

function fakeWindowForSessionStorage(storedItem) {
    var fakeSessionStorage = {
        getItem: sinon.stub().returns(storedItem),
        setItem: sinon.spy(function setItem(key, obj) {storedItem = obj;})
    };
    var fakeWindow = {
        sessionStorage: fakeSessionStorage
    };

    return function ($provide) {
        $provide.value('$window', fakeWindow);
    };
}

And you use it like this:

beforeEach(function() {
    bard.module('app.customers', mySpecHelper.fakeWindowForSessionStorage());
    bard.inject(this, '$location', '$rootScope', '$state', '$templateCache', '$window');
});

I strongly recommend replacing the $window definition itself as we do here rather than replacing/overriding members of $window; if you do the latter, you'll be replacing members of the global browser object, ruining them for all subsequent tests. Of course you could restore the original member after each test with an afterEach but that's a lot of work. Stick with my approach.

zardaloop commented 9 years ago

Ward many thanks for taking time to explain this to me, I really appreciate that. It all make perfect sense. I am only not clear about one thing. Does that mean if I use $provide.provider like this:

$provide.provider('$window', function () {
            /* jshint validthis:true */
            this.$window = sinon.stub();

            this.$get = function () {
                return {
                    sessionStorage: sinon.stub({
                        token: 'someToken'
                    })
                };
            };
        });

It will override the $window for all the other subsequent tests whereas if I only use $provide.value like this:

var fakeSessionStorage = {
        getItem: sinon.stub().returns(storedItem),
        setItem: sinon.spy(function setItem(key, obj) {storedItem = obj;})
    };
    var fakeWindow = {
        sessionStorage: fakeSessionStorage
    };

    return function ($provide) {
        $provide.value('$window', fakeWindow);
    };

it will enable me to fake it for only the bard.module that I am passing on thefakeWindow and afterward the actual $window will be used for the other bard.module if I don't fake it and simply inject $window?

wardbell commented 9 years ago

Both approaches replace the ng $window service definition for each test separately. There is no cross test pollution and test outside of the scope of the describe are unaffected.

The risk of crosps test pollution arises when you do something like $window.sessionStorage=fakeStorage; // don't do this! with the original $window service. That would override the DOM window.sessionStorage which is a global variable.

BTW, the following line in your code doesn't do what you think it does and is immediately replaced anyway at runtime when Ng invkes the $get

this.$window = sinon.stub();
zardaloop commented 9 years ago

I see .... :) Many thanks Ward. Really helpful points. I will keep them all in mind. Cheers

wardbell commented 9 years ago

Closing because there does not appear to be an outstanding issue