Files
gh-secondsky-sap-skills-ski…/references/testing.md
2025-11-30 08:55:36 +08:00

20 KiB

SAPUI5 Testing Guide

Source: Official SAP SAPUI5 Documentation Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials Last Updated: 2025-11-21


Testing Strategy

SAPUI5 applications should include:

  1. Unit Tests (QUnit): Test individual functions and modules
  2. Integration Tests (OPA5): Test user interactions and flows
  3. Mock Server: Simulate backend for testing without real services

Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials (search: testing, qunit, opa)


QUnit Testing

Unit testing framework for JavaScript code.

Setup

test/unit/unitTests.qunit.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Unit Tests</title>

    <script
        id="sap-ui-bootstrap"
        src="../../resources/sap-ui-core.js"
        data-sap-ui-theme="sap_horizon"
        data-sap-ui-resourceroots='{
            "com.mycompany.myapp": "../../"
        }'
        data-sap-ui-async="true">
    </script>

    <link rel="stylesheet" type="text/css" href="../../resources/sap/ui/thirdparty/qunit-2.css">
    <script src="../../resources/sap/ui/thirdparty/qunit-2.js"></script>
    <script src="../../resources/sap/ui/qunit/qunit-junit.js"></script>
    <script src="../../resources/sap/ui/qunit/qunit-coverage.js"></script>

    <script src="unitTests.qunit.js"></script>
</head>
<body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
</body>
</html>

test/unit/unitTests.qunit.js:

QUnit.config.autostart = false;

sap.ui.require([
    "com/mycompany/myapp/test/unit/model/formatter"
], function() {
    "use strict";
    QUnit.start();
});

Writing Unit Tests

test/unit/model/formatter.js:

sap.ui.define([
    "com/mycompany/myapp/model/formatter",
    "sap/ui/model/resource/ResourceModel"
], function(formatter, ResourceModel) {
    "use strict";

    QUnit.module("Formatter Tests");

    QUnit.test("Should format price correctly", function(assert) {
        // Arrange
        var fPrice = 123.456;

        // Act
        var sResult = formatter.formatPrice(fPrice);

        // Assert
        assert.strictEqual(sResult, "123.46 EUR", "Price formatted correctly");
    });

    QUnit.test("Should return empty string for null price", function(assert) {
        // Arrange & Act
        var sResult = formatter.formatPrice(null);

        // Assert
        assert.strictEqual(sResult, "", "Empty string for null");
    });

    QUnit.test("Should format status text", function(assert) {
        // Arrange
        var sStatus = "A";

        // Act
        var sResult = formatter.formatStatus(sStatus);

        // Assert
        assert.strictEqual(sResult, "Approved", "Status formatted correctly");
    });

    QUnit.test("Should calculate total", function(assert) {
        // Arrange
        var iQuantity = 5;
        var fPrice = 10.5;

        // Act
        var fTotal = formatter.calculateTotal(iQuantity, fPrice);

        // Assert
        assert.strictEqual(fTotal, 52.5, "Total calculated correctly");
    });
});

Async Tests

QUnit.test("Should load data asynchronously", function(assert) {
    // Indicate async test
    var done = assert.async();

    // Arrange
    var oModel = new JSONModel();

    // Act
    oModel.loadData("test/data/products.json");

    // Assert after data loaded
    oModel.attachRequestCompleted(function() {
        var aProducts = oModel.getProperty("/products");
        assert.ok(aProducts.length > 0, "Data loaded successfully");
        done(); // Mark test complete
    });
});

Testing Controllers

sap.ui.define([
    "com/mycompany/myapp/controller/Main.controller",
    "sap/ui/thirdparty/sinon",
    "sap/ui/thirdparty/sinon-qunit"
], function(MainController) {
    "use strict";

    QUnit.module("Main Controller", {
        beforeEach: function() {
            this.oController = new MainController();
        },
        afterEach: function() {
            this.oController.destroy();
        }
    });

    QUnit.test("Should initialize correctly", function(assert) {
        // Arrange - spy on method
        var oSpy = sinon.spy(this.oController, "_loadData");

        // Act
        this.oController.onInit();

        // Assert
        assert.ok(oSpy.calledOnce, "Load data called on init");
    });
});

Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials (search: qunit)


OPA5 Testing

One Page Acceptance testing for integration tests.

Setup

test/integration/opaTests.qunit.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Integration Tests</title>

    <script
        id="sap-ui-bootstrap"
        src="../../resources/sap-ui-core.js"
        data-sap-ui-theme="sap_horizon"
        data-sap-ui-resourceroots='{
            "com.mycompany.myapp": "../../"
        }'
        data-sap-ui-async="true">
    </script>

    <link rel="stylesheet" type="text/css" href="../../resources/sap/ui/thirdparty/qunit-2.css">
    <script src="../../resources/sap/ui/thirdparty/qunit-2.js"></script>
    <script src="../../resources/sap/ui/qunit/qunit-junit.js"></script>

    <script src="opaTests.qunit.js"></script>
</head>
<body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
</body>
</html>

test/integration/opaTests.qunit.js:

QUnit.config.autostart = false;

sap.ui.require([
    "sap/ui/test/Opa5",
    "com/mycompany/myapp/test/integration/arrangements/Startup",
    "com/mycompany/myapp/test/integration/WorklistJourney"
], function(Opa5, Startup) {
    "use strict";

    Opa5.extendConfig({
        arrangements: new Startup(),
        viewNamespace: "com.mycompany.myapp.view.",
        autoWait: true
    });

    QUnit.start();
});

Page Objects

test/integration/pages/Worklist.js:

sap.ui.define([
    "sap/ui/test/Opa5",
    "sap/ui/test/actions/Press",
    "sap/ui/test/actions/EnterText",
    "sap/ui/test/matchers/AggregationLengthEquals",
    "sap/ui/test/matchers/PropertyStrictEquals"
], function(Opa5, Press, EnterText, AggregationLengthEquals, PropertyStrictEquals) {
    "use strict";

    var sViewName = "Worklist";

    Opa5.createPageObjects({
        onTheWorklistPage: {
            actions: {
                iPressOnTheFirstListItem: function() {
                    return this.waitFor({
                        id: "table",
                        viewName: sViewName,
                        actions: new Press(),
                        errorMessage: "The table is not visible"
                    });
                },

                iEnterSearchTerm: function(sSearchTerm) {
                    return this.waitFor({
                        id: "searchField",
                        viewName: sViewName,
                        actions: new EnterText({ text: sSearchTerm }),
                        errorMessage: "Search field not found"
                    });
                },

                iPressTheAddButton: function() {
                    return this.waitFor({
                        id: "addButton",
                        viewName: sViewName,
                        actions: new Press(),
                        errorMessage: "Add button not found"
                    });
                }
            },

            assertions: {
                theTableShouldHaveAllEntries: function() {
                    return this.waitFor({
                        id: "table",
                        viewName: sViewName,
                        matchers: new AggregationLengthEquals({
                            name: "items",
                            length: 10
                        }),
                        success: function() {
                            Opa5.assert.ok(true, "Table has 10 entries");
                        },
                        errorMessage: "Table does not have expected entries"
                    });
                },

                theTableShouldBeVisible: function() {
                    return this.waitFor({
                        id: "table",
                        viewName: sViewName,
                        success: function(oTable) {
                            Opa5.assert.ok(oTable.getVisible(), "Table is visible");
                        },
                        errorMessage: "Table is not visible"
                    });
                },

                iShouldSeeTheCorrectTitle: function() {
                    return this.waitFor({
                        id: "page",
                        viewName: sViewName,
                        matchers: new PropertyStrictEquals({
                            name: "title",
                            value: "Worklist"
                        }),
                        success: function() {
                            Opa5.assert.ok(true, "Page title is correct");
                        },
                        errorMessage: "Page title is incorrect"
                    });
                }
            }
        }
    });
});

Journeys (Test Scenarios)

test/integration/WorklistJourney.js:

sap.ui.define([
    "sap/ui/test/opaQunit",
    "./pages/Worklist",
    "./pages/Object"
], function(opaTest) {
    "use strict";

    QUnit.module("Worklist Journey");

    opaTest("Should see the table with all entries", function(Given, When, Then) {
        // Arrangements
        Given.iStartMyApp();

        // Actions
        // (none - just checking initial state)

        // Assertions
        Then.onTheWorklistPage.theTableShouldBeVisible();
        Then.onTheWorklistPage.theTableShouldHaveAllEntries();

        // Cleanup
        Then.iTeardownMyApp();
    });

    opaTest("Should be able to search for items", function(Given, When, Then) {
        // Arrangements
        Given.iStartMyApp();

        // Actions
        When.onTheWorklistPage.iEnterSearchTerm("Product");

        // Assertions
        Then.onTheWorklistPage.theTableShouldHaveAllEntries();

        // Cleanup
        Then.iTeardownMyApp();
    });

    opaTest("Should navigate to object page when item is pressed", function(Given, When, Then) {
        // Arrangements
        Given.iStartMyApp();

        // Actions
        When.onTheWorklistPage.iPressOnTheFirstListItem();

        // Assertions
        Then.onTheObjectPage.iShouldSeeTheObjectPage();

        // Cleanup
        Then.iTeardownMyApp();
    });
});

Startup Arrangements

test/integration/arrangements/Startup.js:

sap.ui.define([
    "sap/ui/test/Opa5"
], function(Opa5) {
    "use strict";

    return Opa5.extend("com.mycompany.myapp.test.integration.arrangements.Startup", {
        iStartMyApp: function() {
            this.iStartMyUIComponent({
                componentConfig: {
                    name: "com.mycompany.myapp",
                    async: true,
                    manifest: true
                },
                hash: "",
                autoWait: true
            });
        }
    });
});

Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials (search: opa5, integration-testing)


Mock Server

Simulate OData backend for testing.

Setup

localService/metadata.xml: Copy metadata from real service or create manually.

localService/mockdata/Products.json:

[
    {
        "ProductID": 1,
        "Name": "Product 1",
        "Price": "100.00",
        "CategoryID": 1
    },
    {
        "ProductID": 2,
        "Name": "Product 2",
        "Price": "200.00",
        "CategoryID": 2
    }
]

localService/mockserver.js:

sap.ui.define([
    "sap/ui/core/util/MockServer",
    "sap/base/util/UriParameters"
], function(MockServer, UriParameters) {
    "use strict";

    return {
        init: function() {
            // Create mock server
            var oMockServer = new MockServer({
                rootUri: "/sap/opu/odata/sap/SERVICE_SRV/"
            });

            // Configure mock server
            var sPath = sap.ui.require.toUrl("com/mycompany/myapp/localService");

            MockServer.config({
                autoRespond: true,
                autoRespondAfter: 1000 // 1 second delay
            });

            // Simulate backend
            oMockServer.simulate(sPath + "/metadata.xml", {
                sMockdataBaseUrl: sPath + "/mockdata",
                bGenerateMissingMockData: true
            });

            // Custom request handling
            var aRequests = oMockServer.getRequests();

            // Add custom response for specific request
            aRequests.push({
                method: "GET",
                path: new RegExp("Products\\?.*"),
                response: function(oXhr) {
                    oXhr.respondJSON(200, {}, JSON.stringify({
                        d: {
                            results: [
                                // Custom data
                            ]
                        }
                    }));
                    return true;
                }
            });

            oMockServer.setRequests(aRequests);

            // Start mock server
            oMockServer.start();

            return oMockServer;
        }
    };
});

test/initMockServer.js:

sap.ui.define([
    "com/mycompany/myapp/localService/mockserver"
], function(mockserver) {
    "use strict";

    // Initialize mock server
    mockserver.init();

    // Initialize application
    sap.ui.require([
        "sap/ui/core/ComponentSupport"
    ]);
});

Start with Mock Server:

<!-- In test HTML file -->
<script src="test/initMockServer.js"></script>

Conditional Mock Server (development only):

sap.ui.define([
    "sap/base/util/UriParameters"
], function(UriParameters) {
    "use strict";

    var oUriParameters = new UriParameters(window.location.href);
    var bUseMockServer = oUriParameters.get("useMockServer") === "true";

    if (bUseMockServer) {
        sap.ui.require([
            "com/mycompany/myapp/localService/mockserver"
        ], function(mockserver) {
            mockserver.init();
            initApp();
        });
    } else {
        initApp();
    }

    function initApp() {
        sap.ui.require(["sap/ui/core/ComponentSupport"]);
    }
});

Usage: index.html?useMockServer=true

Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials (search: mock-server)


Test Automation Best Practices

1. Test Structure (AAA Pattern)

QUnit.test("Test description", function(assert) {
    // Arrange - setup test data and conditions
    var oModel = new JSONModel({ value: 10 });

    // Act - execute the function being tested
    var iResult = myFunction(oModel.getProperty("/value"));

    // Assert - verify the result
    assert.strictEqual(iResult, 20, "Result is correct");
});

2. Use Descriptive Test Names

// Good
QUnit.test("Should format currency with two decimal places", function(assert) {});
QUnit.test("Should return empty string when price is null", function(assert) {});

// Bad
QUnit.test("Test 1", function(assert) {});
QUnit.test("Format test", function(assert) {});

3. Test One Thing Per Test

// Good - separate tests
QUnit.test("Should add item to cart", function(assert) {});
QUnit.test("Should calculate correct total", function(assert) {});

// Bad - testing multiple things
QUnit.test("Should add item and calculate total", function(assert) {});

4. Use Spies and Stubs (Sinon.js)

QUnit.test("Should call service method", function(assert) {
    // Arrange
    var oStub = sinon.stub(oService, "getData").returns([]);

    // Act
    oController.loadData();

    // Assert
    assert.ok(oStub.calledOnce, "Service called once");

    // Cleanup
    oStub.restore();
});

5. Clean Up After Tests

QUnit.module("Test Module", {
    beforeEach: function() {
        // Setup before each test
        this.oModel = new JSONModel();
    },
    afterEach: function() {
        // Cleanup after each test
        this.oModel.destroy();
    }
});

6. Use autoWait in OPA5

Opa5.extendConfig({
    autoWait: true // Automatically waits for UI to be ready
});

7. Organize Tests by Feature

test/
├── unit/
│   ├── model/
│   │   └── formatter.js
│   ├── controller/
│   │   └── Main.controller.js
│   └── unitTests.qunit.html
├── integration/
│   ├── pages/
│   │   ├── Worklist.js
│   │   └── Object.js
│   ├── WorklistJourney.js
│   ├── NavigationJourney.js
│   └── opaTests.qunit.html
└── localService/
    ├── mockserver.js
    ├── metadata.xml
    └── mockdata/

Code Coverage

Enable code coverage in QUnit:

test/unit/unitTests.qunit.html:

<script src="../../resources/sap/ui/qunit/qunit-coverage.js"></script>

View Coverage Report:

  1. Run tests in browser
  2. Click "Enable coverage" in QUnit toolbar
  3. View coverage report after tests complete

Coverage Goals:

  • Unit tests: 80%+ coverage
  • Integration tests: Cover all critical user flows

Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials (search: code-coverage)


Continuous Integration

Karma Runner

karma.conf.js:

module.exports = function(config) {
    config.set({
        frameworks: ["ui5"],
        ui5: {
            type: "application",
            configPath: "ui5.yaml",
            paths: {
                webapp: "webapp"
            }
        },
        browsers: ["ChromeHeadless"],
        reporters: ["progress", "coverage"],
        coverageReporter: {
            type: "html",
            dir: "coverage/"
        },
        singleRun: true
    });
};

package.json:

{
    "scripts": {
        "test": "karma start"
    },
    "devDependencies": {
        "karma": "^6.0.0",
        "karma-ui5": "^2.0.0",
        "karma-chrome-launcher": "^3.0.0",
        "karma-coverage": "^2.0.0"
    }
}

Run Tests:

npm test

Testing Tools

UI5 Inspector

Browser extension for debugging SAPUI5 apps:

  • View control tree
  • Inspect control properties
  • View bindings
  • Check performance

Installation: Available for Chrome and Firefox

Support Assistant

Built-in tool for quality checks:

// Enable in code
sap.ui.require(["sap/ui/support/RuleAnalyzer"], function(RuleAnalyzer) {
    RuleAnalyzer.analyze();
});

Or use keyboard: Ctrl+Alt+Shift+S

Documentation: https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials (search: support-assistant)



Note: This document covers testing strategies and best practices for SAPUI5 applications. For specific testing scenarios and advanced techniques, refer to the official documentation links provided.