Search code examples
javascriptunit-testingsinonqunitbackbone-events

Assert that an event was observed


How can I assert, in a QUnit test case, that a specific Backbone event was observed?

The application uses Backbone.js events (Backbone.js version 1.3.3) for communicating between components. A simple view responds to a button click, by triggering a custom event on the event bus:

// foo.js

const WheelView = Backbone.View.extend({

    events: {
        "click #btn-spin-wheel": "onSpinButtonClick",
    },

    onSpinButtonClick: function () {
        console.log("DEBUG: clicked #btn-spin-wheel");
        Backbone.trigger("wheel:spin");
    },
});

I want a QUnit test case (QUnit version 1.22.0) that asserts “when this button is selected, event "foo" appears on the event bus”.

The test case will also need to know other aspects of the event (such as optional arguments), so I need a function defined in the test case that the test case arranges as a callback for the specific event.

This is the latest test case I've tried, by making a Sinon (version 1.9.0) spy function for the event callback:

// test-foo.js

QUnit.module("Button “btn-spin-wheel”", {
    beforeEach: function (assert) {
        this.wheelView = new WheelView();
    },
    afterEach: function (assert) {
        delete this.wheelView;
    },
});

QUnit.test(
    "Should observe the “wheel:spin” event.",
    function (assert) {
        assert.expect(1);

        const spinWheelButton = document.querySelector(
            "#qunit-fixture #btn-spin-wheel");
        const eventListener = sinon.spy(function () {
            console.log("DEBUG:QUnit: received ‘wheel:spin’ event");
        });
        Backbone.once("wheel:spin", eventListener);

        const done = assert.async();
        window.setTimeout(function () {
            spinWheelButton.click();
            window.setTimeout(function () {
                assert.ok(eventListener.calledOnce);
            }.bind(this));
            done();
        }.bind(this), 500);
    });

The console.log invocations are to help me understand which functions are being called and which are not. I expect to see both:

DEBUG: clicked #btn-spin-wheel
DEBUG:QUnit: received ‘wheel:spin’ event

Instead, only the click message appears:

DEBUG: clicked #btn-spin-wheel

This is confirmed because the test case fails:

Button “btn-spin-wheel”: Should observe the “wheel:spin” event. (3, 0, 3)    579 ms
    1.  Assertion after the final `assert.async` was resolved    @ 578 ms
        Source: Assert.prototype.ok@file:///usr/share/javascript/qunit/qunit.js:1481:3

    2.  failed, expected argument to be truthy, was: false    @ 578 ms
        Expected: true
        Result: false
        Source: @file://[…]/test-foo.html:50:29

    3.  Expected 1 assertions, but 2 were run    @ 579 ms
        Source: @file://[…]/test-foo.html:36:13

Source: @file://[…]/test-foo.html:36:13

I have read about QUnit support for asynchronous testing and I am experimenting with different assert.async and setTimeout usages, as suggested in the documentation examples. So far it is to no avail.

How should I use QUnit, Sinon, and Backbone, to assert (the existence, and specific properties of) a specific observed event from the app?


Solution

  • The problem turns out to be an interaction between test cases. The test cases manipulate the listeners and fire events onto the event hub; but all the test cases share the same event hub.

    So when the test cases run asynchronously, instead of the test cases being isolated, events fired in one can affect another.

    The workaround I have implemented is a custom event queue for each test case, that is managed in the QUnit.module.beforeEach and ….afterEach:

    /**
     * Set up an event hub fixture during `testCase`.
     *
     * @param testCase: The QUnit test case during which the fixture
     * should be active.
     * @param eventHub: The Backbone.Events object on which the fixture
     * should exist.
     */
    setUpEventsFixture: function (testCase, eventHub) {
        testCase.eventHubPrevious = eventHub._events;
        eventHub._events = [];
    },
    
    /**
     * Tear down an event hub fixture for `testCase`.
     *
     * @param testCase: The QUnit test case during which the fixture
     * should be active.
     * @param eventHub: The Backbone.Events object on which the fixture
     * should exist.
     */
    tearDownEventsFixture: function (testCase, eventHub) {
        eventHub._events = testCase.eventHubPrevious;
    },
    

    By using these in the test module definitions:

    QUnit.module("Button “btn-spin-wheel”", {
        beforeEach: function (assert) {
            setUpEventsFixture(this, Backbone);
            this.wheelView = new WheelView();
        },
        afterEach: function (assert) {
            delete this.wheelView;
            tearDownEventsFixture(this, Backbone);
        },
    });
    

    The test cases now can continue to use code that uses the common Backbone object as the event hub, but their events are isolated from each other.