A journey with OPA

Presented by Arnaud Buchholz
Presentation made with Reveal.js

Agenda

    Sample application

    training-ui5con18-opa

    Why Automated Testing?

    If it's not tested, it doesn't work

    Need I say more?

    Automated vs manual testing

    As the code complexity grows, it takes more and more time and/or resources to test the software manually.

    The only sustainable solution is to automate the tests and have them run fast.

    Some drawbacks of automated testing

    It takes too much timeSome Product Owners...

    This is a short term vision of automated testing: by reducing the number of bugs and maximizing the quality, you end up doing more features and less bug fixing.

    Go slow to go fast

    Some advantages of automated testing

    Code never lies, comments sometimes doRon Jeffries

    Automated tests ensure the proper documentation of the software: they showcase the application features.

    Tests secure future code, enabling fast non-regression testing.

    Unit testing vs Integration testing

    Maximize unit testing: they must be fast and they ensure code modularity.

    Leverage integration testing: check the happy path then edge cases.

    Assessing test coverage

    There are many ways to measure the tests coverage:

    • Number of scenarios tested
    • Number of acceptance criterias tested
    • Code coverage

    Don't forget the The Pareto principle. (a.k.a. the 80 / 20 rule)

    Limits of code coverage

    Code coverage gives you insights on which lines of code were executed. However, it does not mean that all scenarios were executed.

    
    								function divide (dividend, divisor) {
    									return dividend / divisor;
    								}
    
    								assert(divide(2,2) === 1);
    							

    Experiences feedback

    After one year working with OPA tests, I changed my mind completely concerning OPA.
    It is a great opportunity to verify the robustness and quality of the UI5 developments.
    At the beginning it is hard to start, with the time it becomes much more faster to develop OPA tests.
    Roger Knop

    Experiences feedback

    [...] App seems to be quite stable. The effort for OPA testing (especially at the beginning of the project) pays out! A good example is the Variant Management of the GanttChart [...]. At a certain point in time we thought everything would have been covered. But due to existing OPA tests we figured out, that some functionalities were still missing [...]. Maximilian Rupp

    Experiences feedback

    From my expierence in other projects I already knew the value of testing. But we never did testing to that extent. So developing a new feature was really half implementing the functionality half writing tests. Which was quite some effort and sometimes a challenge. But along the way it really helped us discover errors, weak points and unforseen side effects. And in the end the quality stands for itself. Pascal Wasem

    OPA Overview

    One Page Acceptance Tests

    • UI behavior testing
    • JavaScript based
    • Tightly integrated with UI5
    • Asynchronous with active polling mechanisms
    • Enables Test-Driven Development through abstractions

    Compared to other automation frameworks

    Selenium, Marionette or Puppeteer are framework agnostic, one must rely on the generated HTML to code automation (IDs, CSS selectors...).


    OPA is designed by and for UI5 developers: one manipulates UI5 controls.

    What is OPA?

    OPA is a set of tools used to automate and validate UI (and it comes with backend mocking).


    Tests are organized in Journeys, based on Pages to abstract the UI, and readable.

    An example of one OPA test

    
    							opaTest("should remove completed items", function(Given, When, Then) {
    								// Arrangements
    								Given.iStartTheApp();
    
    								// Actions
    								When.onTheAppPage.iEnterTextForNewItemAndPressEnter(S_NEW_ITEM_TITLE);
    								When.onTheListOfItems.iSetTheItemToCompleted(S_NEW_ITEM_TITLE);
    								When.onTheAppPage.iClearTheCompletedItems();
    
    								// Assertions
    								Then.onTheListOfItems.iShouldNotSeeAnyItemTitled(S_NEW_ITEM_TITLE)
    									.and.iTeardownTheApp();
    							});
    						
    Given, When, Then pattern

    Executing OPA tests

    • As simple as running a web page
    • A qUnit report documents execution results

    • Application can be run in two different modes:
      • As a component within the test window
      • Inside an iFrame (slower but isolated)

    Limits of OPA

    • Designed to test one UI5 application
    • Not designed to test backend

    Pages & Journeys

    Pages

    They are used to abstract UI elements

    Pages Examples

    Pages Actions & Assertions

    • onTheListOfItems
      
      									/* ... */
      									When.onTheListOfItems.iSetTheItemToCompleted(S_NEW_ITEM_TITLE);
      									Then.onTheListOfItems.iShouldSeeTheNewItem(S_NEW_ITEM_TITLE);
      									/* ... */
      								
    • onTheFilterButtons
      
      									/* ... */
      									When.onTheFilterButtons.iClick(filters.COMPLETED);
      									Then.onTheFilterButtons.iShouldSeeTheButtonCount(filters.ALL, 4);
      									/* ... */
      								

    Pages Examples

    Pages Actions & Assertions

    • onTheEditDialog
      
      									/* ... */
      									Then.onTheEditDialog.iShouldSeeTheTitleField();
      									When.onTheEditDialog.iClickClose();
      									/* ... */
      								

    Pages implementation

    
    							sap.ui.define([
    								"sap/ui/test/Opa5"
    							], function(Opa5) {
    								Opa5.createPageObjects({
    									onAbstractNamePage: {
    										/*baseClass: ClassOfferingCommonHelpers*/,
    										actions: {
    											iExecuteAnAction: function() { return this.waitFor(/*...*/); },
    											/*...*/
    										},
    										assertions: {
    											iCheckAnAssertion: function() { return this.waitFor(/*...*/); },
    											/*...*/
    										}
    									}
    								});
    							});
    						

    Makes an extensive use of this.waitFor

    Filters=test/integration/pages/Filters.js

    Journeys

    • Relying on pages' methods, they describe the user experience within the application

    • A journey is composed of tests

    • Each test follows the pattern
      • Given: setup the initial state of the test
      • When: execute actions
      • Then: check assertions

    Journey implementation

    
    							sap.ui.define([
    								"sap/ui/test/opaQunit"
    							], function(opaTest) {
    								QUnit.module("Journey name");
    								opaTest("Test name", function(Given, When, Then) {
    									Given.iStartMyApp();
    									When.onAbstractNamePage.iExecuteAnAction()
    										.and.iExecuteAnotherAction();
    									Then.onAbstractNamePage.iCheckAnAssertion()
    										.and.iTeardownMyAppFrame();
    								});
    								opaTest("Test name 2", function(Given, When, Then) {/*...*/});
    								/*...*/
    							});
    						
    Todo List Journey=test/integration/TodoListJourney.js

    Loading pages & journeys

    • Ensure qUnit is loaded
    • Load OPA, pages & journeys
    • Start
    
    							sap.ui.require([
    								"sap/ui/test/Opa5",
    								/* pages */
    								/* journeys */
    							], function(Opa5) {
    								Opa5.extendConfig({
    									/* Default settings */
    								});
    								QUnit.start();
    							});
    						
    All Journeys=test/integration/AllJourneys.js QUnit.start() is not present in AllJourneys.js because of Continuous Integration

    Anatomy of waitFor

    Presentation of waitFor

    • Main entry point to synchronize tests with the running application

    • Locate and interact with UI5 controls

    waitFor is asynchronous

    waitFor-based members fill an execution queue
    You can wait on the end of this queue using sap.ui.test.Opa5.emptyQueue

    Synchronization in asynchronous tests

    Since the test function is executed before the application is started, waitFor is required to synchronize.
    For instance, to add a breakpoint:

    
    							// Actions
    							When.onTheAppPage.iEnterTextForNewItemAndPressEnter(S_NEW_ITEM_TITLE);
    							When.onTheListOfItems.iSetTheItemToCompleted(S_NEW_ITEM_TITLE);
    
    							When.waitFor({
    								success: function() {
    									debugger;
    								}
    							});
    
    							When.onTheAppPage.iClearTheCompletedItems();
    						

    waitFor options

    • waitFor offers a large set of options
    • They can be grouped and sequenced:
      • (Configuring) timeout
      • Finding
      • Filtering
      • Checking
      • Interacting
      • Reporting
      • (Configuring) autoWait

    Configuring - timeout

    • timeout decides of the maximum waiting time (we use 15s)
    • pollingInterval can be changed (defaulted to 400ms)

    Finding


    • id
    • controlType
    • viewNamespace & viewName
    • searchOpenDialogs can be combined only with controlType
    
    									iEditTheItem: function(sTitle) {
    										return this.waitFor({
    											controlType: "sap.m.ObjectListItem",
    											/* ... */
    										});
    									}
    								

    No criteria means all controls
    Polling until at least one control is found or timeout

    Filtering (matchers)

    Given the resultset of previous step
    For each control:
    
    									iEditTheItem: function(sTitle) {
    										return this.waitFor({
    											controlType: "sap.m.ObjectListItem",
    											matchers: [new Properties({
    												title: sTitle
    											})],
    											/* ... */
    										});
    									}
    								
    Use one of the sap.ui.test.matchers or a function
    Polling until remaining results or timeout

    Checking (check)

    Given the resultset of previous step
    • Accept or reject all results
    
    									iShouldNotSeeAnyItemTitled: function(sTitle) {
    										return this.waitFor({
    											controlType: "sap.m.ObjectListItem",
    											check: function(aItems) {
    												return aItems.every(function(oItem) {
    													return oItem.getTitle() !== sTitle;
    												});
    											},
    											/* ... */
    										});
    									}
    								

    Polling until truthy or timeout

    Interacting (actions)

    Given the resultset of previous step
    • Press
    • EnterText
    • Applied on all controls
    • Adapts to the control
      For instance, Press on a search field will trigger the search and EnterText will enter search criteria.
    
    									iSetTheTitleTo: function(sTitle) {
    										return this.waitFor({
    											id: "title",
    											actions: [new EnterText({
    												text: sTitle
    											})],
    											/* ... */
    										});
    									}
    								
    Use one of the sap.ui.test.actions

    Reporting (success / error / errorMessage)

    • Final step of the waitFor, receives resultset
    • Avoid testing controls' state in success function
    • Can chain to a subsequent waitFor
    
    							iShouldSeeAGivenNumberOfItems: function(iCount) {
    								return this.waitFor({
    									controlType: "sap.m.ListBase",
    									matchers: [new AggregationLengthEquals({
    										name: "items",
    										length: iCount
    									})],
    									success: function() {
    										Opa5.assert.ok(true, "The list shows " + iCount + " items");
    									},
    									errorMessage: "The list doesn't show " + iCount + " items"
    								});
    							}
    						

    Configuring - autoWait

    • Not enabled by default
    • When enabled, waitFor monitors pending tasks requests, timeouts, promises, UI navigation...
      and includes the Interactable matcher
      Controls states are tested to check that they are 'available' and 'interactable'

    • Use sap.ui.test.Opa5.extendConfig to set it globally
    • Can be turned off in a specific waitFor
      For instance if you want to check that a button is disabled

    Configuring - visible

    • To find controls that are not visible from a UI5 point of view
    
    							// iShouldNotSeeTheField(sId, sName)
    							return this.waitFor({
    								autoWait: false,
    								visible: false,
    								id: sId,
    								matchers: [new Properties({
    									visible: false
    								})],
    								success: function() {
    									Opa5.assert.ok(true, "The " + sName + " field is *not* displayed");
    								},
    								errorMessage: "The " + sName + " field is displayed"
    							});
    						

    Mocking Backend

    The need for a backend

    When the application has a backend counterpart, it can't run without being connected to it.

    However, tests have to rely on a stable dataset and some features might be dangerous.

    Introducing the MockServer


    The MockServer is a software component that captures AJAX requests and either answer them or let them reach the backend.

    Configuring the MockServer

    • It requires the metadata to know the entities and relationships exposed by the ODATA service
    • It initializes the entity sets by generating them or loading JSON files
    
    							_oMockServer = new MockServer({
    								rootUri: "/odata/TODO_SRV/"
    							});
    							_oMockServer.simulate("model/metadata.xml", {
    								sMockdataBaseUrl: "model/"
    							});
    							_oMockServer.start();
    						
    Check the documentation of sap.ui.core.util.MockServer

    MockServer entities

    At any time, one can manipulate the entities

    
    							var aExisting = _oMockServer.getEntitySetData("TodoItemSet"),
    								sGuid = "0MOCKSVR-TODO-MKII-MOCK-00009999";
    							// Adding a new entity
    							aExisting.push({
    								"Guid": sGuid,
    								"Title": "Generated",
    								"Completed": false,
    								"DueDate": "/Date(" + new Date(2099, 11, 31).getTime() + ")/",
    								"__metadata": {
    									id: "/odata/TODO_SRV/TodoItemSet(guid'" + sGuid + "')",
    									uri: "/odata/TODO_SRV/TodoItemSet(guid'" + sGuid + "')",
    									type: "TODO_SRV.TodoItem"
    								}
    							});
    							_oMockServer.setEntitySetData("TodoItemSet", aExisting);
    						
    ?randomize=test/MockServer.js#L83

    Default MockServer hooks

    By default, the MockServer hooks a lot of
    ODATA operations :

    • $batch
      (transparently split into individual requests and consolidates results)
    • Create, Read, Update and Delete
    • Query parameters: paging, filtering, sorting
    • Resolving navigation properties
      One level only

    But function imports are not supported

    Altering the MockServer hooks

    An hook is defined with:

    • method: GET, POST, PUT...
    • path: a regexp matching the API URL
      Using capturing group, you can extract url parameters
    • response function: where the behavior is implemented

    Altering the MockServer hooks

    
    							var aRequests = _oMockServer.getRequests();
    							aRequests.push({ // Creation of a todo list item
    								method: "POST",
    								path: CONST.OData.entityNames.todoItemSet,
    								response: function(oXhr) {
    									/* ... */
    									// Initialize some fields
    									var oBody = JSON.parse(oXhr.requestBody);
    									oBody[CONST.OData.entityProperties.todoItem.completed] = false;
    									oBody[CONST.OData.entityProperties.todoItem.completionDate] = null;
    									oXhr.requestBody = JSON.stringify(oBody);
    									return false; // Keep default processing
    								}
    							});
    							_oMockServer.setRequests(aRequests);
    						
    Creation of a todo list item=test/MockServer.js#L115

    Adding new MockServer hooks

    This is how function import can be implemented

    
    							// Clear Completed
    							aRequests.push({
    								method: CONST.OData.functionImports.clearCompleted.method,
    								path: CONST.OData.functionImports.clearCompleted.name,
    								response: function(oXhr) {
    									/* ... */
    									oXhr.respond(200, {
    										"Content-Type": "application/json;charset=utf-8"
    									}, JSON.stringify({
    										d: oResult
    									}));
    									return true; // Skip default processing
    								}
    							});
    						
    Clear Completed=test/MockServer.js#L173

    MockServer hooks Tips & Tricks

    • The MockServer does not need to be 100% equivalent to the backend
    • Newly added hooks are executed first (LIFO)
    • You may also use attachBefore or attachAfter callbacks triggered before or after request processing

    Key Take-Aways

    • If it's not tested, it doesn't work
    • OPA is designed to automate and validate UI5 applications
    • Asynchronous with active polling
    • Define pages to abstract and componentize UI
    • Keep the journeys readable
    • Use MockServer to simulate the backend
    • training-ui5con18-opa