Presented by Arnaud Buchholz
Presentation made with Reveal.js
If it's not tested, it doesn't work
Need I say more?
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.
It takes too much time
Some 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
Code never lies, comments sometimes do
Ron Jeffries
Automated tests ensure the proper documentation of the software: they showcase the application features.
Tests secure future code, enabling fast non-regression testing.
Maximize unit testing: they must be fast and they ensure code modularity.
Leverage integration testing: check the happy path then edge cases.
There are many ways to measure the tests coverage:
Don't forget the The Pareto principle. (a.k.a. the 80 / 20 rule)
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);
After one year working with OPA tests, I changed my mind completely concerning OPA.Roger Knop
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.
[...] 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
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
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.
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.
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
They are used to abstract UI elements
/* ... */
When.onTheListOfItems.iSetTheItemToCompleted(S_NEW_ITEM_TITLE);
Then.onTheListOfItems.iShouldSeeTheNewItem(S_NEW_ITEM_TITLE);
/* ... */
/* ... */
When.onTheFilterButtons.iClick(filters.COMPLETED);
Then.onTheFilterButtons.iShouldSeeTheButtonCount(filters.ALL, 4);
/* ... */
/* ... */
Then.onTheEditDialog.iShouldSeeTheTitleField();
When.onTheEditDialog.iClickClose();
/* ... */
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
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) {/*...*/});
/*...*/
});
sap.ui.require([
"sap/ui/test/Opa5",
/* pages */
/* journeys */
], function(Opa5) {
Opa5.extendConfig({
/* Default settings */
});
QUnit.start();
});
waitFor-based members fill an execution queue
You can wait on the end of this queue using
sap.ui.test.Opa5.emptyQueue
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();
iEditTheItem: function(sTitle) {
return this.waitFor({
controlType: "sap.m.ObjectListItem",
/* ... */
});
}
iEditTheItem: function(sTitle) {
return this.waitFor({
controlType: "sap.m.ObjectListItem",
matchers: [new Properties({
title: sTitle
})],
/* ... */
});
}
iShouldNotSeeAnyItemTitled: function(sTitle) {
return this.waitFor({
controlType: "sap.m.ObjectListItem",
check: function(aItems) {
return aItems.every(function(oItem) {
return oItem.getTitle() !== sTitle;
});
},
/* ... */
});
}
iSetTheTitleTo: function(sTitle) {
return this.waitFor({
id: "title",
actions: [new EnterText({
text: sTitle
})],
/* ... */
});
}
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"
});
}
// 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"
});
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.
The MockServer is a software component that captures AJAX requests and either answer them or let them reach the backend.
_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
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);
By default, the MockServer hooks a lot of
ODATA operations :
But function imports are not supported
An hook is defined with:
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);
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
}
});
If it's not tested, it doesn't work