Search code examples
knockout.jsqunit

Assert that a function was executed


I am trying to unit test a Knockout JS extender function that subscribes to a ko.observable (causing it to run when the value changes). To test that it works correctly, I need to verify that the extender function was executed when the ko.observable was changed.

Here is my test so far:

test("ko.extenders.addFieldTrackingGA", function () {

    //arrange
    var testObservable = ko.observable(1).extend({
         addFieldTrackingGA: "Some button was clicked"
    });

    //act
    testObservable(5);

    //assert

});

My question is: How can I assert that ko.extenders.addFieldTrackingGA was executed when the observable was changed?

Here is the code that I want to confirm executed:

Knockoutextension:

ko.extenders.addFieldTrackingGA = function (target, option) {
    target.subscribe(function (newValue) {
        if (newValue) {
            qb.Utils.Analytics().trackEvent(qb.Utils.Analytics().product,
                                            "form click",
                                            option,
                                            false);
        }
    });
    return target;
};

qb.analystics:

 /**
 * Event = e.g. 'trackEvent'
 * Category = e.g. 'error_message_home'
 * Action = fieldName
 * Label = 'some message'
 * ignoreMultiple = false | true | {blank} - if true, gtm actions that are fired more than once will be ignored, defaults to true.
 */
var _pushGTM = function (event, category, action, label, ignoreMultiple) {
    if (typeof dataLayer !== 'undefined') { // Add test for dataLayer as breaking Qunit

        ignoreMultiple = ignoreMultiple === undefined ? true : ignoreMultiple;

        if (_.contains(pushedGTM, action + label) && ignoreMultiple) { // Make sure event doesn't get fired more than once, only fire it the first time
            return;
        }

        var gtmObject = {
            'event': event,
            'eventDetails.category': category,  // Push the value depending on the form (car/house/contents)
            'eventDetails.action': action,      // Push the form field name.(If there is no field name push "No_field"
            'eventDetails.label': label         // Please push the exact error string. 
        }

        if (ignoreMultiple) {
            pushedGTM.push(action + label);
        }

        _pushGTMObject(gtmObject);
    }
}

Solution

  • You should realize that you have a pretty heavy dependency in your extender: qb.analytics. Currently you're (by side effect) also testing that, when you only want to test one unit: the extender.

    I can give you at least three basic options to handle this:

    1. Factor out the dendency on qb and inject it somehow into your extender. Your tests can then inject mocks that help with the assertion.
    2. Use some kind of spy/mock framework like SinonJS. I'll admit, because of lack of experience with SinonJS and similar frameworks I'm not 100% sure if this approach will pay off.
    3. Monkey patch qb in your tests to help with assertions.

    The latter approach is a bit blunt, but it's straightforward. Here's how it would work:

    (function() {
      "use strict";
    
      var trackEventFn = function() { };
    
      QUnit.module("Mymodule", {
        beforeEach: function() {
          window.qb = {
            Utils: {
                Analytics: function() {
                  return { trackEvent: trackEventFn };
                }
            }
          };
        }
      });
    
      QUnit.test("ko.extenders.addFieldTrackingGA", function(assert) {
        // arrange
        var testObservable = ko.observable(1).extend({
          addFieldTrackingGA: "Some button was clicked"
        });
    
        // prepare assertion    
        trackEventFn = function(prod, event, option, somebool) {
          assert.ok(true);
          // You can also assert on the argument values here
        }
        assert.expect(1); // Or more than 1, depending on the above
    
        // act
        testObservable(5);
      });
    
    }());
    

    Here's a full demo:

    window.qb = {
      Utils: {
        Analytics: function() {
          return { trackEvent: function() { } };
        }
      }
    };
    
    ko.extenders.addFieldTrackingGA = function (target, option) {
      target.subscribe(function (newValue) {
        if (newValue) {
          qb.Utils.Analytics().trackEvent(qb.Utils.Analytics().product,
                                          "form click",
                                          option,
                                          false);
        }
      });
      return target;
    };
    
    (function() {
      "use strict";
    
      var trackEventFn = function() { };
    
      QUnit.module("Mymodule", {
        beforeEach: function() {
          window.qb = {
            Utils: {
              Analytics: function() {
                return { trackEvent: trackEventFn };
              }
            }
          };
        }
      });
    
      QUnit.test("ko.extenders.addFieldTrackingGA", function(assert) {
        // arrange
        var testObservable = ko.observable(1).extend({
          addFieldTrackingGA: "Some button was clicked"
        });
    
        // prepare assertion    
        trackEventFn = function(prod, event, option, somebool) {
          assert.ok(true);
          // You can also assert on the argument values here
        }
        assert.expect(1); // Or more than 1, depending on the above
    
        // act
        testObservable(5);
      });
    
    }());
    <link href="https://cdnjs.cloudflare.com/ajax/libs/qunit/1.18.0/qunit.min.css" rel="stylesheet"/>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/qunit/1.18.0/qunit.min.js"></script>
    
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>