6
\$\begingroup\$

Using JavaScript/jQuery I created a simple toggle and started writing unit tests for it using Jasmine. I am not sure, however, if I should be testing for things like e.g. if "click" listener is registered when object is instantiated. Should this be done as part of unit test or rather E2E test using WebDriver or something? At the moment, I'm mostly checking if e.g. toggle function changes content of HTML as expected but am a bit lost in how far I should take my unit testing.

MySwitch = function(options) {

    this.options = options;
    this._$el = $(options.element);
    this._status = 0;
    this._$el.data("value-on") ? this._valueOn = this._$el.data("value-on") : this._valueOn = "ON";
    this._$el.data("value-off") ? this._valueOff = this._$el.data("value-off") : this._valueOff = "OFF";
    if (!this._$el.text()) this._$el.text(this._valueOff);

    this._$el.on("click", $.proxy(this.toggle, this));
};

MySwitch.prototype.toggle = function(el) {

    this._status ? (this._$el.text(this._valueOff), this._status = 0) : (this._status = 1, this._$el.text(this._valueOn));

    if (typeof this.options.onToggle === "function")
        this.options.onToggle.call(this, this._status);
};

MySwitch.prototype.getStatus = function(el) {

    return this._status;
};

And tests:

describe("My switch", function() {

    var options, element, myToggle;

    beforeEach(function() {

    element = document.createElement("div");

    element.innerHTML = '<span data-value-off="AM" data-value-on="PM"></span>"';

    $("body")[0].appendChild(element);

    function onToggleCallback() {};

    options = {

        element: $(element).find("span")[0],
        onToggle: onToggleCallback
    };

    myToggle = new MySwitch(options);
});

afterEach(function() {

    $("body > div")[0].remove();

});

it("Should set ON/OFF as default values", function() {

    $("body > div")[0].remove();

    element = document.createElement("div");

    element.innerHTML = '<span></span>';

    $("body")[0].appendChild(element);

    options = {

        element: $(element).find("span")[0],
        onToggle: function(status) {}
    };

    myToggle = new MySwitch(options);
    $(element).removeAttr("data-value-on").removeAttr("data-value-off");

    expect($(element).text()).toMatch("OFF");

    myToggle.toggle();

    expect($(element).text()).toMatch("ON");

});

it("Should have AM as off value", function() {

    expect($(element).text()).toMatch("AM");

});

it("Should have PM as on value", function() {

    expect($(element).text()).toMatch("AM");

});

it("Should toggle AM->PM->AM", function() {

    myToggle.toggle();

    expect($(element).text()).toMatch("PM");

    myToggle.toggle();

    expect($(element).text()).toMatch("AM");
});

it("Should call onToggle callback", function() {

    spyOn(options, "onToggle");

    myToggle.toggle();

    expect(options.onToggle).toHaveBeenCalled();

    });

});
\$\endgroup\$
2
\$\begingroup\$

To begin with, don't interact with the DOM. The reason for that is imagine that you have hundreds of unit tests. Do you want to clean up the DOM after them? Are you going to forget sometimes? If you forget will you be able to find why unit tests are failing? When you will be setting up a Continuous Integration process which will run the unit tests will you use a JavaScript engine tool that is aware of a DOM? What I'm getting at is only test the logic.

In order to make your class unit testable it needs to be refactored. To begin with, you need to make the constructor not do anything except set the this.options and this._status and move all the other code to an init method:

MySwitch = function(options) {
    this.options = options;
    this._status = 0;
};

MySwitch.prototype.init = function() {
    this._$el = $(options.element);
    this._$el.data("value-on") ? this._valueOn = this._$el.data("value-on") : this._valueOn = "ON";
    this._$el.data("value-off") ? this._valueOff = this._$el.data("value-off") : this._valueOff = "OFF";
    if (!this._$el.text()) this._$el.text(this._valueOff);

    this._$el.on("click", $.proxy(this.toggle, this));  
};

Now refactor the toggle method:

MySwitch.prototype.toggle = function() {
    this.toggleStatus();
    this.setText();
    this.runToggleFunction();
};

MySwitch.prototype.runToggleFunction = function() {
    if (typeof this.options.onToggle === "function")
        this.options.onToggle.call(this, this._status);
};

MySwitch.prototype.toggleStatus = function() {
    this._status = this._status === 0 ? 1 : 0;
};

MySwitch.prototype.setText = function() {
    this.getEl.text( this._status ? this._valueOn : this._valueOff );
};

MySwitch.prototype.getEl = function() {
    return this._$el;
};

Now you can test the code:

describe("MySwitch", function() {

    it("toggle()", function() {
        var mySwitch = new MySwitch();

        spyOn(mySwitch, 'toggleStatus');
        spyOn(mySwitch, 'setText');
        spyOn(mySwitch, 'runToggleFunction');

        mySwith.toggle();

        expect(mySwitch.toggleStatus).toHaveBeenCalled();
        expect(mySwitch.setText).toHaveBeenCalled();
        expect(mySwitch.runToggleFunction).toHaveBeenCalled();
    });

    describe('runToggleFunction()', function() {
        it('callback provided', function() {
            var func = jasmine.createSpy('func');
            var mySwitch = new MySwitch({onToggle: func});
            var status = 1;

            mySwitch._status = status;
            mySwitch.runToggleFunction();

            expect(func).toHaveBeenCalledWith(status);
        });

        it('no callback', function() {
            var mySwitch = new MySwitch();

            try {
                mySwitch.runToggleFunction();
                expect(true).toBe(true);
            } catch (e) {
                expect(true).toBe(false);
            }
        });
    });

    it('toggleStatus()', function() {
        var mySwitch = new MySwitch();

        mySwitch._status = 1;
        mySwitch.toggleStatus();

        expect(mySwitch._status).toBe(0);
    });

    describe('setText()', function() {

        var valueOn = "on";
        var valueOff = "off";
        var mySwitch;
        var $el;

        beforeEach(function() {
            mySwitch = new MySwitch();

            $el = {
                text: function(){}
            };

            spyOn($el, 'text');
            spyOn(mySwitch, 'getEl').and.returnValue($el);
        });

        it('status 0', function() {
            mySwitch._status = 0;
            mySwitch.setText();
            expect($el.text).toBe(valueOff);
        });

        it('status 1', function() {
            mySwitch._status = 1;
            mySwitch.setText();
            expect($el.text).toBe(valueOn);
        });
    });

});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.