Good Practices
In this sections you will learn some good practices that will help you improve the quality of your Minium tests.
One Elements
expression, one Interaction
Most of UI testing and automation code should be as simple as identifying elements and perform interactions with those elements. For that reason, Minium code should be as straighforward as possible:
- an expression should be chosen to evaluate into an element
- an interaction should be applied to that expression
- explicit waits sould be avoided if possible
- getting values from elements like text, size, etc. should also be avoided
An example of what Minium code should look like:
// elements expressions
var usernameFld = $(":text").withName("username");
var passwordFld = $(":password").withName("password");
var loginBtn = $(":submit");
// interactions
usernameFld.fill("minium");
passwordFld.fill("some strong password");
loginBtn.click();
Avoid control flow statements
Control flow statements like if-then-else
should be an exception, not the rule, and so, try to aoid using them.
As an example, suppose you want to uncheck all checkboxes in a form. A possible way of doing it could be:
var checkboxes = $("#my-form :checkbox");
for (var checkbox in Iterator(checkboxes)) {
if (checkbox.is(":checked")) checkbox.click();
}
However, it is possible to do the same without the if
statement. The right way would be:
var checkboxes = $("#my-form :checkbox").filter(":checked");
for (var checkbox in Iterator(checkboxes)) {
checkboxes.click();
}
Avoid explicit waits
Let's consider filling a field when some loading operation is running. If we want to ensure that we don't fill that field until the loading operation completes, one possible solution would be:
// elements
var field = $("#somefield");
var loading = $(".loading");
// interactions
loading.waitForUnexistence();
field.fill("Minium can do better than this");
However, we can simplify that code by just considering a different expression for field
: if the expression
only evaluates into #somefield
element when .loading
element doesn't exist, we can avoid having an explicit
wait:
// elements
var field = $("#somefield").unless(".loading");
// interactions
field.fill("Minium can!");
Avoid getting values from elements
Minium provides some jquery methods for accessing values from WebElements
, like '.text()', '.attr(name)', etc.
However, these methods will always evaluate immediatelly, which can be a problem, because Minium cannot
ensure their evaluation occurs when it actually evaluates into a non-empty set. For that reason, it should be avoided.
Instead, filters are provided to restrict elements based on a specific value:
// gets element by text
$("span").withText("Minium can!");
// gets element by style value
$("div").withCss("visibility", "visible");
// gets element by name
$(":text").withName("somename");
In case you really need to get some value, consider chaining the method call with a .waitForExistence()
:
var elemText = $("label").waitForExistence().text();
// or getting a style value
var backgroundColor = $("label").waitForExistence().css("background-color");
Avoid iterating using .size()
Let's say we want to remove all items from a shopping cart, and for that we need to iterate through all the corresponding remove buttons and click on it. We could consider the following code for that:
var shoppingCart = $("#shopping-cart");
var removeBtns = shoppingCart.find(".items button").withValue("Remove");
var size = removeBtns.size();
for (var i = 0; i < size; i++) {
// only clicks the first matching element
removeBtn.click();
}
However, as explained before, getting values from elements should be avoided, and so a better alternative is to replace
the iteration with a while
loop where we check for the existence of removeBtns
(don't forget that
each iteration will remove the first element, therefore next time removeBtns
evaluates it will return
one element less):
var shoppingCart = $("#shopping-cart");
var removeBtns = shoppingCart.find(".items button").withValue("Remove");
while (removeBtns.checkForExistence()) {
// only clicks the first matching element
removeBtns.click();
}
Yet, that code is not perfect yet: when no more items exist in the shopping cart, Minium will wait until a timeout at
removeBtns.checkForExistence()
and only then it will return false, breaking the while
loop.
On the other hand, if we try to check immediatelly for the existence of removeBtns
(with removeBtns.checkForExistence("immediate")
),
that could cause some problems. Let's, for instance, make things even harder: consider that every time we remove an element of the shopping
cart, the shopping cart is refreshed dynamically (an element .loading
will be displayed).
In that case, removing an item would mean that the next iteration using removeBtns.checkForExistence("immediate")
would not occur,
because removeBtns
would not exist until the shopping cart completed refreshing.
So, how do we avoid breaking the loop too soon and avoid waiting the timeout
period at the end?
The solution is the following:
var shoppingCart = $("#shopping-cart").unless(".loading");
var removeBtns = shoppingCart.find(".items button").withValue("Remove");
while (shoppingCart.waitForExistence().then(removeBtns).checkForExistence("immediate")) {
// only clicks the first matching element each iteration
removeBtns.click();
}
So what did we change?
- we changed the shopping cart expression to
$("#shopping-cart").unless(".loading")
, so that we only consider the shopping cart element when no loading is in progress - we changed the
while
loop condition to:- ensure shopping cart exists (
shoppingCart.waitForExistence()
) - we use the
.then(removeBtns)
method to returnremoveBtns
expression only whenshoppingCart.waitForExistence()
evaluates into a non-empty set - we can now check immediatelly for the existence of
removeBtns
, because we know that no loading operation is in progress
- ensure shopping cart exists (
Avoid calling browser
methods outside any of Cucumber methods
Avoid at all cost calling browser
methods outside any of cucumber methods (World
, When
, Then
, etc.). That can afect tests in multiple browsers, as it will force all browsers to open on startup instead of opening the browser when it is needed (that is, when tests for that specific browser start to run). This can cause selenium timeouts because of browsers being opened too long without any activity.
Instead, consider initializing browser
inside of World
:
World(function () {
var loadingElem = $(".loading");
browser.configure()
.interactionListeners()
.clear()
.add(minium.interactionListeners.ensureUnexistence(loadingElem));
});