29

document.cookie is like a string, but it is not a string. To quote the example from the Mozilla doc:

document.cookie = "name=oeschger";
document.cookie = "favorite_food=tripe";
alert(document.cookie);
// displays: name=oeschger;favorite_food=tripe

If you tried to make a mock cookie using only a string, you would not get the same results:

var mockCookie = "";
mockCookie = "name=oeschger";
mockCookie = "favorite_food=tripe";
alert(mockCookie);
// displays: favorite_food=tripe

So, if you wanted to unit test a module that operates on the cookie, and if you wanted to use a mock cookie for those tests, could you? How?

thisgeek
  • 3,396
  • 4
  • 24
  • 26

7 Answers7

22

You could create an object with a cookie setter and getter. Here is a very simple implementation:

var mock = {
    value_: '', 

    get cookie() {
        return this.value_;
    },

    set cookie(value) {
        this.value_ += value + ';';
    }
};

Might not work in all browsers though (especially IE). Update: It only works in browsers supporting ECMAScript 5!

More about getter and setters.

mock.cookie = "name=oeschger";
mock.cookie = "favorite_food=tripe";
alert(mock.cookie);
// displays: name=oeschger;favorite_food=tripe;

DEMO

Ramon Snir
  • 7,520
  • 3
  • 43
  • 61
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • So, what you imply is that you can, of course, mimic the behavior, but the interface _cannot_ be the same. Is that right? – thisgeek Jun 23 '11 at 15:23
  • @thisgeek: Not sure what you mean... it is the same interface or am I missing something? – Felix Kling Jun 23 '11 at 15:24
  • I misread what you wrote. There was no `:` between `get` and `cookie`, but my mind put it there anyway. That means your solution depends on ECMAScript 5 support in the JS engine, which I can live with. – thisgeek Jun 23 '11 at 15:36
  • +1 but note that the "get/set" (and `__defineGetter__`/`__defineSetter__`) constructs are specified by ECMA-262 5th Edition (ECMAScript 5) which, as you mention, is *not* supported by MSIE but is supported by most other browsers. – maerics Jun 23 '11 at 15:39
9

This implementation allows overwriting cookies, and adds document.clearCookies()

(function (document) {
    var cookies = {};
    document.__defineGetter__('cookie', function () {
        var output = [];
        for (var cookieName in cookies) {
            output.push(cookieName + "=" + cookies[cookieName]);
        }
        return output.join(";");
    });
    document.__defineSetter__('cookie', function (s) {
        var indexOfSeparator = s.indexOf("=");
        var key = s.substr(0, indexOfSeparator);
        var value = s.substring(indexOfSeparator + 1);
        cookies[key] = value;
        return key + "=" + value;
    });
    document.clearCookies = function () {
        cookies = {};
    };
})(document);
mcintyre321
  • 12,996
  • 8
  • 66
  • 103
  • Join should be made with a "; " separator to follow actual implementation: `return output.join("; ");` – Alvis Oct 09 '17 at 09:05
8

@Felix Kling's answer is right on, I just wanted to point out that there is an alternate syntax for defining setters and getters in ECMAScript 5:

function MockCookie() {
  this.str = '';
  this.__defineGetter__('cookie', function() {
    return this.str;
  });
  this.__defineSetter__('cookie', function(s) {
    this.str += (this.str ? ';' : '') + s;
    return this.str;
  });
}
var mock = new MockCookie();
mock.cookie = 'name=oeschger';
mock.cookie = 'favorite_food=tripe';
mock.cookie; // => "name=oeschger;favorite_food=tripe"

And again, most browsers support ECMAScript 5 (defined by ECMA-262 5th Edition) but not MSIE (or JScript).

Joe Lencioni
  • 10,231
  • 18
  • 55
  • 66
maerics
  • 151,642
  • 46
  • 269
  • 291
6

I figured out that jasmine has spyOnProperty which can be used for when you want to spy on getter and setters of objects. So I solved my issue with this:

const cookie: string = 'my-cookie=cookievalue;';    
spyOnProperty(document, 'cookie', 'get').and.returnValue(cookie);
user2125726
  • 127
  • 2
  • 10
5

Here is how I ended up doing it in Jest:

My cookie wasn't added anymore to document.cookie after adding the Secure attribute (because document.location is the insecure http://localhost).

So after lots of trials and errors (trying to intercept document.cookie's setter and dropping its ;Secure from it, but then calling again document.cookie = with the new value triggered an infinite loop as it entered again the setter...), I ended up with this quite simple solution:

beforeEach(function() {
  let cookieJar = document.cookie;
  jest.spyOn(document, 'cookie', 'set').mockImplementation(cookie => {
    cookieJar += cookie;
  });
  jest.spyOn(document, 'cookie', 'get').mockImplementation(() => cookieJar);
})
Yann Dìnendal
  • 1,422
  • 2
  • 17
  • 27
2

I know this is an old topic, but in my case expiring cookies was necessary so here's a solution that combines the above answers and a setTimeout call to expire cookies after X seconds:

const fakeCookies = {
    // cookie jar
    all: {},

    // timeouts
    timeout: {},

    // get a cookie
    get: function(name)
    {
        return this.all[ name ]
    },

    // set a cookie
    set: function(name, value, expires_seconds)
    {
        this.all[ name ] = value;

        if ( expires_seconds ) {
            ! this.timeout[ name ] || clearTimeout( this.timeout[ name ] )
            this.timeout[ name ] = setTimeout(() => this.unset(name), parseFloat(expires_seconds) * 1000)
        }
    },

    // delete a cookie
    unset: function(name)
    {
        delete this.all[ name ]
    }    
}
Ismail
  • 725
  • 8
  • 23
1

Personally i was unable to hijack the document object. A simple solution which seems to work for me was the following...

At the top of my test script i define a fakeCookie object:

var fakeCookie = {
    cookies: [],
    set: function (k, v) {
        this.cookies[k] = v;
    },
    get: function (k) {
        return this.cookies[k];
    },
    reset: function () {
        this.cookies = [];
    }
};

Then in my beforeEach() i define my cookie stub. This basically intercepts calls to jQuery.cookie and (instead!) call the callback function that i have defined (see below):

beforeEach(function() {
    var cookieStub = sinon.stub(jQuery, "cookie", function() {
        if (arguments.length > 1) {
            fakeCookie.set(arguments[0], arguments[1]);
        }
        else {
            return fakeCookie.get(arguments[0]);
        }
    });
});

Any time that i get or set a cookie value it uses my fakeCookie instead of the real jQuery.cookie. It does this by looking at the number of parameters passed and deduces whether its a get/set. I literally pasted this in and it all worked straight off the bat. Hope this helps!!

omarjebari
  • 4,861
  • 3
  • 34
  • 32