Skip to content

Latest commit

 

History

History
351 lines (253 loc) · 10.3 KB

cookbook-for-testing-controls-with-qunit-0ddcc60.md

File metadata and controls

351 lines (253 loc) · 10.3 KB

Cookbook for Testing Controls with QUnit

Test Cases

You can use a factory function. To keep this pointer and have a descriptive message, you should use the test inside of the function and pass a test name to it.

Internally, we prefer to pass an object to the test for retrieving the values - it makes the test cases readable.

// "Bar" required from module "sap/m/Bar"
// "nextUIUpdate" required from module "sap/ui/test/utils/nextUIUpdate"
function renderBarInPageTestCase(sTestName, oOptions) {
    QUnit.test(sTestName, async(assert) => { 
        // System under Test
        const oBar = new Bar();

        oBar.placeAt("qunit-fixture");

        // Act
        oBar.applyTagAndContextClassFor(oOptions.context);

        await nextUIUpdate();

        // Assert
       assert.strictEqual(oBar.getDomRef().nodeName, oOptions.expectedTag.toUpperCase());
       assert.ok(oBar.$().hasClass(oOptions.expectedClass), "The bar has the context class: " + oOptions.expectedClass);

        // Cleanup
        oBar.destroy();
    });
};

renderBarInPageTestCase("Should render the header context", {
    context : "header",
    expectedTag : "header",
    expectedClass : "sapMHeader-CTX"
});

renderBarInPageTestCase("Should render the header context", {
    context : "subheader",
    expectedTag : "header",
    expectedClass : "sapMSubHeader-CTX"
});

renderBarInPageTestCase("Should render the header context", {
    context : "footer",
    expectedTag : "footer",
    expectedClass : "sapMFooter-CTX"
});

Testing User Interactions

When testing user interactions, you can use sap.ui.test.qunit to trigger events.

Here is an example for when a user presses [Esc] on the select:

// "Item" required from module "sap/ui/core/Item"
// "Select" required from module "sap/m/Select"
// "KeyCodes" required from module "sap/ui/events/KeyCodes"
// "nextUIUpdate" required from module "sap/ui/test/utils/nextUIUpdate"
// "QUnitUtils" required from module "sap/ui/qunit/QUnitUtils"
QUnit.test("Should close the popup menu if it is open and you press escape", async(assert) => {
    // Arrange
    const oConstructor = {
        items: [
        new Item({
            key: "0",
            text: "item 0"
        }),

        new Item({
            key: "1",
            text: "item 1"
        })
       ]
    };

    // System under test
    const oSelect = new Select(oConstructor);

    oSelect.placeAt("select-content");
    await nextUIUpdate();

    // Arrange after rendering
    oSelect.focus();
    const fnEscapeSpy = this.spy(oSelect, "onsapescape");
    const fnCloseSpy = this.spy(oSelect, "close");

    // Act
    QUnitUtils.triggerKeydown(oSelect.getDomRef(), KeyCodes.ESCAPE);

    // Assertion
    assert.strictEqual(fnEscapeSpy.callCount, 1, "onsapescape() method was called exactly once");
    assert.strictEqual(fnCloseSpy.callCount, 0, "close() method is not called");

    // Cleanup
    oSelect.destroy();
});

In the rendering tests part, you have to place your control in the DOM. The best place to put it is the qunit-fixture div, since its content gets deleted after every test.

Make sure you destroy your control, since SAPUI5 will keep a reference to it and may also rerender it.

It's crucial that you wait for the Promise of sap/ui/test/utils/nextUIUpdate after each time you've caused a rerendering.

Executing the module returns a Promise which resolves after rendering. If you don't wait for the Promise, the DOM won't be updated yet.

You can use the following template to make sure that you remember to destroy your control:

// "nextUIUpdate" required from module "sap/ui/test/utils/nextUIUpdate"
QUnit.test("Should do Something", async(assert) => {
    // Arrange
    const oConstructor = {

    };

    // System under Test
    const oMyControl = new nameSpace.myControl(oConstructor);
    oMyControl.placeAt("qunit-fixture");

    // Wait for the rendering before proceeding the test
    await nextUIUpdate();

    // Act

    // Assert

    // Cleanup
    oMyControl.destroy();
});

Caution:

Using nextUIUpdate in combination with fake timers has some pitfalls. If fake timers are used, you need to pass the clock of the fake timer to the nextUIUpdate function call.

When working with a fake timer, keep in mind to either execute all timeouts before restoring the fake timer or to make sure that no rendering is pending. Not executing the pending timeouts can lead to issues within the following tests, for example by interupting the rendering lifecycle.

Example usage of nextUIUpdate in combination with a fake timer:

// nextUIUpdate with fake timer
QUnit.test("Test with fake timers", async() => {
    this.clock = sinon.useFakeTimers();

    // Coding which requires rendering, e.g.
    const oButton = new Button();
    oButton.placeAt("qunit-fixture");

    await nextUIUpdate(this.clock);

    // Continue with your test

    // Release all timers at the end of the test
    // to avoid e.g. blocking the rendering within
    // following tests and to ensure that other
    // timeouts triggered through integration are
    // released
    oButton.destroy()
    this.clock.runAll();
    this.clock.restore();
}

In the example below, you will test to see whether the control fails to rerender. The control has overwritten the setter of the tooltip property to avoid triggering a re-rendering.

To test this, we add an eventDelegate to see how often the rendering function is called. We need to make sure that we wait for the next rendering:

// "Label" required from module "sap/m/Label"
// "nextUIUpdate" required from module "sap/ui/test/utils/nextUIUpdate"
QUnit.test("Should suppress rerendering when tooltip is set", async(assert) => { 
    // Arrange
    const oConstructor = {
        tooltip : "foo"
        };
    const oRerenderingSpy = this.spy();

    // System under Test
    const oLabel = new Label(oConstructor);
    oLabel.placeAt("qunit-fixture");

    // wait for rendering
    await nextUIUpdate();

    oLabel.addEventDelegate({
        onBeforeRendering : oRerenderingSpy
    });

    // Act
    oLabel.setTooltip("bar");

   // wait for rendering
    await nextUIUpdate();

    // Assert
    assert.strictEqual(oRerenderingSpy.callCount, 0, "Did not rerender");
    assert.strictEqual(oLabel.getTooltip(), "bar", "Tooltip property got set");
    assert.strictEqual(oLabel.$().attr("title"), "bar", "Tooltip got updated");

    // Cleanup
    oLabel.destroy();
});

Testing with Server-Side Models

For bindable properties of your control, it should be possible to bind them to server-side models, i.e. models for which not all data is available on the client but is loaded asynchronously from a server. This is especially important for more complex controls, e.g. with an aggregation which may be bound to a collection loaded using paging. A typical example for server-side models are UI5's OData models. When testing with models, you need to make sure that you also set up/destroy the model itself inside your test. In the following, we will show an example using the OData V2 mock server:

// "MockServer" required from module "sap/ui/core/util/MockServer"

function startMockServer(iRespondAfter) {
    // configure respond to requests delay
    MockServer.config({
        autoRespond : true,
        autoRespondAfter : iRespondAfter || 10
    });

    // create mockserver
    const oMockServer = new MockServer({
        rootUri : "http://sap.com/service/"
    });

    // start and return
    oMockServer.simulate("data/metadata.xml", "data");
    oMockServer.start();
    return oMockServer;
}

//Your test:
QUnit.test("Should do something with the model", function (assert) {
    //Arrange
    const oMockServer = startMockServer(0);
    
    // System under Test + Act

    //Cleanup
    oMockServer.stop();
});

When using the OData V2 mock server, you can use async tests since calling respond each time on the mock server does not help the readability of the test.

After setting up the OData V2 mock server, we set up the model as follows:

// "ODataModel" required from module "sap/ui/model/odata/v2/ODataModel"
// "jQuery" required from module "sap/ui/thirdparty/jquery"

function createODataModel(sURL, mSettings) {
    sURL = sURL || "http://sap.com/service/";
    const oModel = new ODataModel(sURL);
    
    mSettings = mSettings || {};
    jQuery.each(mSettings, function(sProperty, vValue) {
        sProperty = sProperty[0].toUpperCase() + sProperty.substring(1);
        oModel["set" + sProperty](vValue);
    });
    
    return oModel;
}

//Your test:
QUnit.test("Should do something with the model", function(assert) {
    // Arrange
    const oModel = createODataModel();
    const oMockServer = startMockServer(0);
    const done = assert.async();

    // System under Test + Act + call done();

    // Cleanup
    oModel.destroy();
    oMockServer.stop();
});

You can now bind your model against your control and test whatever you want.

We use clock.tick to trigger the server response. If you didn't do this, the text of the label would still be empty:

// "Label" required from module "sap/m/Label"
// "nextUIUpdate" required from module "sap/ui/test/utils/nextUIUpdate"

//Your test:
QUnit.test("Should do something with the model", async(assert) => {
    // Arrange
    const oModel = createODataModel();
    const oMockServer = startMockServer(50);

    // System under Test
    const oLabel = new Label({
        text : "{/myProperty}"
    });

    oLabel.placeAt("qunit-fixture");
    await nextUIUpdate();

    // Act - trigger the request
    sinon.clock.tick(50);

    // Assert
    assert.strictEqual("myExpected", oLabel.getText(), "The expected text was present");

    // Cleanup
    oModel.destroy();
    oMockServer.stop();
    sinon.clock.reset() 
});
sap.ui.require(["sap/ui/core/Theming"], (Theming) => {
    QUnit.test("Check if theme change was done correctly", function(assert){
        const myTestFunction = () => {
            // Test changes are applied
            Theming.detachApplied(myTestFunction);
        }
        Theming.setTheme("myTestTheme");
        Theming.attachApplied(myTestFunction);
    });
});