Technical Articles
Testing UI5 apps, part 2: Integration- aka OPA Testing
Testing UI5 apps
-
- Part1: Setup and Unit Testing
- Part2: Integration aka OPA Testing (this article)
- Part3.1: Mockserver
- Part3.2: Code Coverage and other necessary Usefulities
- Part4: Advanced Testing mumbo-jambo
- Part5: Numbers, Experiences and Business Impact
After starting off with Setup and Unit Testing in the first part of the blog series, let’s shift from the functional perspective to the integration aspect.
I’d like to think of integration tests as a means of making sure that -upon changes to the UI 5 application-, it continues to implement the most important business cases successfully. All that nice UX and UI boils down to the UI5 app fulfilling a purpose – and integration tests verify that the UI interaction in place allows for that to happen. Or as Arnaud Buchholz put it at UI5con 2018: “it checks the happy path” 🙂
In UI5-verse, integration tests are performed with “One Page Acceptance Tests” (or OPA(5) for brevity). It’s an extension of QUnit and operates the UI of the application just as a user would.
(repeat hint from Blog Part 1: the code for this blog series along with installation instructions is located at https://github.com/vobujs/openui5-sample-app-testing – yep, by the URL you can already tell that I copied the official sample ToDo UI5 app and modified it to fit the purpose of this blog series about testing.)
Bootstrapping manual Integration Tests
Similar to how Unit Tests are bootstrapped for manual browser testing, OPA Tests are started via a dedicated HTML file:
<!DOCTYPE html>
<html>
<head>
<title>OPA tests for Todo List</title>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta charset="utf-8">
<script id="sap-ui-bootstrap"
src="https://openui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-theme="sap_belize"
data-sap-ui-bindingSyntax="complex"
data-sap-ui-compatVersion="edge"
data-sap-ui-preload="async"
data-sap-ui-resourceRoots='{"sap.ui.demo.todo": "../../"}'>
</script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-2-css.js"></script>
<script>
QUnit.config.autostart = false;
sap.ui.getCore().attachInit(function() {
sap.ui.require([
"sap/ui/demo/todo/test/integration/AllJourneys"
], function() {
QUnit.start();
});
});
</script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>
After UI5’s core has booted, OPA (in its QUnit-incarnation) is started and will execute AllJourneys.js
: the container file for all Test-Journeys.
sap.ui.require([
"sap/ui/test/Opa5"
/* Pages */
/* Journeys */
], function (Opa5) {
Opa5.extendConfig({
autoWait: true
/* Default settings */
});
});
Recommendation: turn on autoWait
in default config. This excludes checking inactive or non-interactable Controls, so turn it off in specific (see below) waitFor
s (e.g. in test senarios when Message toasts are involved that only disappear after n
number of seconds). But it will help in waiting for asynchronous operations on the UI to complete before test execution continues.
Tests are organized in “Journeys”, based on “Pages”:
├── AllJourneys.js
├── FilterJourney.js
├── SearchJourney.js
├── TodoListJourney.js
├── opaTests.qunit.html
└── pages
├── App.js
└── Common.js
Journeys represent the use cases, Pages abstract UI elements and test their status and/or content.
Anatomy of OPA Journeys
The structure of an OPA Journey follows a basic pattern:
Given
sets up the journey/test.
When
gets the UI and environment into the state mandatory to test the business case.
Then
performs the main assessment.
opaTest("description", function (Given, When, Then) {
// Arrangements
Given.iStartTheApp();
// Actions
When.onTheAppPage.iEnterTextAndPressEnter("some text");
// Assertions
Then.onTheAppPage.iShouldSeeTheText("some text")
.and.iTeardownTheApp();
});
Heads up to possible Arrangement-/Given
-methods:
it’s possible to start the UI5 app under test either in an iFrame (via iStartMyAppInAFrame
) or by launching the Component only (via iStartMyUIComponent
). The latter is significantly faster, easier to debug and also the “Fiori Launchpad way” of starting a UI5 app, but there’s advantages and disadvantages to both approaches. There’s also a cool blog post on a custom launcher, simulating starting an app from the FLP à la “iStartMyUIComponentInUshell”.
Similar to QUnit-based Unit Tests, OPA journeys can be grouped via QUnit.module("GroupName")
:
QUnit.module("Todo List");
opaTest(...);
opaTest(...);
opaTest(...);
QUnit.module("Footer");
opaTest(...);
opaTest(...);
Anatomy of OPA pages
By convention, the actions and assertions of an OPA test are grouped in “page” files, corresponding to their UI5 view parts.
sap.ui.require([
"sap/ui/test/Opa5"
], function (Opa5) {
"use strict";
Opa5.createPageObjects({
onTheAppPage: {
/* common methods outsourced in separate file "Common.js" */
baseClass: Common,
actions: {
iEnterTextForNewItemAndPressEnter: function (text) {
return this.waitFor({
/* */
});
}
},
assertions: {
iShouldSeeTheItemBeingAdded: function (iItemCount, sLastAddedText) {
return this.waitFor({
/* */
});
}
}
}
});
});
As you can see from the coding, OPA (pages) make extensive use of sap.ui.test.Opa5.waitFor()
– a method generating a Promise, used to synchronize application state with test state.
At the same time, this is the most important aspect of OPA testing to remember:
OPA tests need to be synchronized with the application.
sap.ui.test.Opa5.waitFor()
is the code tool for that.
So no matter what UI element or interaction you want to test, it needs to go into a waitFor
. The documentation has a solid list of how to retrieve and operate Controls, so here’s only an excerpt for some cases:
Finding a Control
- by Id in a View:
byId: function(sId) {
return this.waitFor({
id: sId,
viewName: "sap.ui.demo.todo.App",
success : function (oControl) {
/* assert sth on oControl */
}
})
}
- without Id, but a specific property value; note the use of one of the many sap.ui.test.matchers:
withHelpOfMatchers: function (sText) {
return this.waitFor({
controlType: "sap.m.Button",
viewName: "sap.ui.demo.todo.App",
// require sap.ui.test.matchers.PropertyStrictEquals and
// use as PropertyStrictEquals
matchers: new PropertyStrictEquals({
name: "text",
value: sText
}),
success: function (oButton) {
/* assert sth on oButton */
}
})
}
- in a Dialog or Popup; attention: no finding by Id possible here, only by
controlType
:
inPopup: function (sControlTypeInPopup, sText) {
return this.waitFor({
controlType: sControlTypeInPopup,
// require sap.ui.test.matchers.PropertyStrictEquals and
// use as PropertyStrictEquals
matchers: new PropertyStrictEquals({
name: "text",
value: sText
}),
searchOpenDialogs: true,
success: function (oButton) {
/* assert sth on oButton */
}
})
}
Interacting with a Control
Don’t fall back to using some variation of jQuery(oControl.getDomRef()).trigger()
, but use the OPA-native sap.ui.test.actions
instead. Again, the documentation has some good examples, so only note the basic code concept here:
iEnterTextForSearchAndPressEnter: function (sId, sViewName, sText) {
return this.waitFor({
id: sId,
viewName: sViewName,
// require sap.ui.test.actions.EnterText and
// use as EnterText
actions: [new EnterText({text: sText})],
errorMessage: "The text cannot be entered"
});
}
Useful OPA thingies to know
subsequent time-outs
If there’s some error in the asynchronous execution of the OPA test queue, it is very likely that subsequent tests (Page element functions) after the failed one error out too, even though they might work standalone.
This can happen due to the default timeout for sap.ui.test.Opa.config
of 15 seconds: they might have eventually already been passed in the test case throwing the error. So all following tests run into the default timeout.
Don’t utilize Controls in success callback function
Again, OPA’s main feature is to sync test state with application state. If interactions are performed in the success
handler of a waitFor
, chances are that the application’s UI is off timing-wise and the interaction fails:
// will fail occasionally!
this.waitFor({
id: "listItemId",
// dependening on the processing power of the computer executing the test,
// the list might not be entirely rendered and thus DOM operation on
// list item nodes not available yet
success: function (oListItem) {
// double-bad: jQuery instead of sap.ui.test.actions.Press
oListItem.$("bla").trigger("tap");
// ...
}
}
It is more reliable to use waitFor
‘s actions
to interact with UI5 controls, along with autoWait
:
this.waitFor({
id: "listItemId",
actions: new Press() // require: sap.ui.test.actions.Press as "Press",
autoWait: true
}
Test for wording
Sure, the programming world is an English-speaking one, but quite often UI5 applications run in multi-language environments. So testing for English UI texts only is a bad idea:
// e.g. with a French system/browser, this will fail
matchers: [new PropertyStrictEquals({
name: "text",
value: iNumberItemsLeft + (iNumberItemsLeft === 1 ? " item left" : " items left")
})
Instead, use OPA’s I18NText
-matcher: it will retrieve the correct text from the appropriate i18n-model for the browser runtime environment.
// ...
oItemLeftMatcher = new I18NText({
propertyName: "text",
key: "ITEM_LEFT"
}
// ...
// multi-lang match of label text
return this.waitFor({
id: sItemsLeftLabelIdText,
viewName: sViewName,
matchers: oItemLeftMatcher,
success: function () {
Opa5.assert.ok(true, "" + iNumberItemsLeft + " items left");
},
errorMessage: "Items are not selected."
});
Debug application state
Given that OPA aka QUnit-extension queues up all Tests before executing them and waitFor
additionally adds test-application synchronization, it’s hard to debug between the journey steps (Given
-Arrangements, When
-Actions and Then
-Assertions).
So insert an empty waitFor
to break into application runtime and test state:
opaTest("again: should unselect an item", function (Given, When, Then) {
// Arrangements
Given.iStartTheApp();
//Actions
When.onTheAppPage.iEnterTextForNewItemAndPressEnter("my test")
.and.iSelectAllItems(true)
.and.iClearTheCompletedItems();
// debug the test and UI here!});
When.waitFor({
success: function() {
debugger;
}
});
When.onTheAppPage.iEnterTextForNewItemAndPressEnter("my test")
.and.iSelectTheLastItem(true)
.and.iSelectTheLastItem(false);
// Assertions
Then.onTheAppPage.iShouldSeeTheLastItemBeingCompleted(false).
and.iTeardownTheApp();
});
Conclusion
Integration- aka OPA-Testing helps to ensure successful business case execution upon changes to the UI5 application.
OPA Tests are organized in pages that are used in journeys to describe the UI interaction necessary for the business case.
On a technical level, sap.ui.test.Opa5.waitFor()
is the programming means to sync test state and application state in order to avoid timing-based errors. Use its’ matchers
, checks
and actions
extensively to find and interact with UI5 controls.
In the next blog post of the series, we’ll look at utilities that help with a broader test coverage.
Hey thank you for your explanation on integrationtests!
I try to write also an opatest. In this test I have to deal with an uploadcollection (with a dialogfile) for uploading pdf´s.
How does an integration test work there?
Hi Liem,
I got the same OPA5 test requirement like yours, regarding file uploader and I've got no clue. just wondering if you have your solution worked? and if yes could you kindly share the key parts?
Thanks a lot in advance,
Dongcheng
I'm facing the same challenge here.