This article shows a pattern for Test Automation that embraces the testing (validation) of web applications that allow different spoken languages (EN, IT etc). It also presents a novel way of writing BDD statements that improves the Test Automation development and maintenance process.
2. 2
CONTENTS
INTRODUCTION........................................................................... 3
THE APPLICATION........................................................................ 4
Use Cases ............................................................................... 4
Pages...................................................................................... 6
CODE ADAPTIONS ....................................................................... 9
THE ROLE OF BDD ..................................................................... 13
TRANSLATION MECHANISM ........................................................ 13
Translator Class ..................................................................... 13
Translation Data..................................................................... 15
WORKFLOW EXAMPLE ................................................................ 16
BDD Statements..................................................................... 16
Page Objects.......................................................................... 17
Step Definitions...................................................................... 21
Hooks ................................................................................... 26
Execution .............................................................................. 27
SUMMARY................................................................................. 27
3. 3
INTRODUCTION
In today’s globalized business world, online applications need to consider their
customer’s experience in the context of spoken language.
For example, banking, shipping, travel, insurance, and general shopping
applications populate domains that have identified the need to embrace
customer language. This engagement can happen, in overall application
workflow, at the “Sign-In” or “Sign-Up” stage. Indeed, such language
dependency can extend to the data being displayed, where it represents, for
example, a language-based account or contract that a user has with the
business or legal terms and conditions governing transactions.
The internationalization (i18n) process is quite well established as part of web
application architecture. Indeed, in some development groups the translation of
textual matter is handled by a centralised team, enhancing consistency of
terminology both within individual applications as well as across the wider
business application landscape.
For the test automator this multi-lingual aspect presents a challenge. This is
particularly so in the situation where test automation is focused, as this author
would strongly recommend, on testing rather than simply operating as a robot,
asserting that all is well by simply getting to the end of a user journey
expressed in a test without the application complaining or, in the worst case,
crashing. At the heart of testing, whether manual or automated, lies the key act
of asserting/validating.
In this article an example will be presented, being the (Sign-In/Sign-
Up/Remember-Me/Recover-Password) business workflow, one which lies at the
heart of many web application today. The application in which this workflow
exists has been described in an earlier article1.
The testing approach here is based on the use of Java, BDD [here] (Cucumber
[here], Gherkin [here]) and some observations will be made on how such
behavioural statement structures should be viewed.
The extensibility of the approach to those situations where the test automation
code can connect directly to a development-level translation resource, will also
be described.
1
UI Testing Pattern, https://www.slideshare.net/DavidHarrison20/
4. 4
THE APPLICATION
In the earlier work we looked at a single area of a business application and
developed UI tests to exemplify a proposed general pattern of test
development. The main landing page of this multi-lingual React-based
application is shown below:
As a general observation, UI testing is most effective in the QA activity of a
project when it is used to assert user journey (workflow) correctness. As noted
above the (Sign-In/Sign-Up/Remember-Me/Recover-Password) area of an
application is an important one to assert as correct.
USE CASES
Specifically, we will look at the following use cases:
Case Comments
New User Sign-Up Using a unique email, SignUp as a
new user. Following this test, the
newly signed up user is removed from
the database
New User Sign-Up Revert In this workflow the user reverts to
the SignIn page (Cancel) from the
CreateWorkspace page, which is
shown following the SignUp page
5. 5
Existing User Sign-In Using an existing test account, the
user signs into the application
Existing User Password Recovery Full Using an existing test account, the
user requests password recovery
(Forgot Password). The recovered
password is sent to a pre-configured,
language-dependent, email inbox
(specified in the BDD statements)
Existing User Password Recovery Revert Using an existing test account, the
user reverts to SignIn page from the
PasswordRecover page
Sign-In Credentials not recognised A user attempts to sign in using
credentials that are not recognised.
This test ensures that the SignIn page
helper text is correctly displayed
Sign-In Helper Text Valid This test ensures that the SignIn page
helper text is correctly displayed
Sign-Up Helper Text Valid This test ensures that the SignUp
page helper text is correctly displayed
Remember Me Toggle This test ensures that the “remember
me” toggle offered on the SignIn page
is working as expected
The workflow can be visualised as shown in the following diagram:
SignInPage DashboardPage
SignUpPage
REVERT
CREATE
SIGN-UP
CreateWorkspacePage
SIGN-IN
NOT-RECOGNISED
ForgotPasswordPage
RECOVER-PASSWORD
REVERT
RECOVER-PASSWORD
REVERT
INVALID
6. 6
PAGES
The pages involved in the workflow are as shown below:
SignInPage
Helper text displayed case.
Note that at SignIn-time the user can select a language by clicking on one of
the flag icons, below the “SIGN IN” button.
8. 8
CreateWorkspacePage
Helper text displayed case.
This panel will be displayed in the language selected on the initial SignIn page
(see above).
ForgotPasswordPage
Helper text displayed case.
This panel will be displayed in the language chosen on the SignIn page (see
above).
9. 9
DashboardPage
These individual pages of the application are represented as Page Object
classes in the test project as we will see below.
CODE ADAPTIONS
There are two key adaptions to the front-end React code that are made in order
that we can deliver an overall workable and reliable testing solution. The
changes centre on adding custom attributes to visual and textual elements of
the UI. These are described in the following table:
Custom Attribute Name Comments
data-test-id This attribute is added to all elements of the UI
that need to be discovered
data-trans-key This attribute is added to all textual elements that
are subject to translation. The value of this
attribute represents a key into a translation data
store, retrieving the expected textual value for any
of the targeted spoken languages
The naming convention applied to the custom attributes is in line with what is
allowed in the React/Material-UI framework, i.e., all custom attribute names
must start with “data-“.
10. 10
If we look at a key fragment of the Typescript code for the SignInPage, the
details of the adaptions can be seen:
. . .
return (
<Wrapper
style={{
backgroundImage: `url(${
backgroundImageUrl || '/images/signin.jpg'
})`,
}}
>
<Content>
<Logo>
{logoUrl ? (
<img
src={logoUrl}
width="240px"
alt={i18n('app.title')}
/>
) : (
<h1 data-test-id="label.title" data-trans-key="sign-in-page.label.title">{i18n('app.title')}</h1>
)}
</Logo>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<InputFormItem
name="email"
label={i18n('user.fields.email')}
autoComplete="email"
autoFocus
externalErrorMessage={externalErrorMessage}
pageName="sign-in-page"
/>
<InputFormItem
name="password"
label={i18n('user.fields.password')}
autoComplete="password"
type="password"
pageName="sign-in-page"
/>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<FormControlLabel
control={
<Checkbox
id={'rememberMe'}
name={'rememberMe'}
defaultChecked={true}
inputRef={form.register}
color="primary"
size="small"
data-test-id="check-box.remember-me"
data-test-backend-pid="6960"
/>
}
label={i18n('user.fields.rememberMe')}
data-test-id="label.remember-me"
data-trans-key="sign-in-page.link.remember-me"
/>
<MaterialLink
style={{ marginBottom: '8px' }}
component={Link}
to="/auth/forgot-password"
id={'materialLink.forgotPassword'}
11. 11
data-test-id="link.forgot-password"
data-trans-key="sign-in-page.link.forgot-password"
>
{i18n('auth.forgotPassword')}
</MaterialLink>
</Box>
<Button
style={{ marginTop: '8px' }}
variant="contained"
color="primary"
type="submit"
fullWidth
disabled={loading}
id={'button.signIn'}
data-test-id="button.sign-in"
data-trans-key="sign-in-page.button.sign-in"
>
{i18n('auth.signin')}
</Button>
<OtherActions>
<MaterialLink
component={Link}
to="/auth/signup"
id={'materialLink.createAnAccount'}
data-test-id="link.create-an-account"
data-trans-key="sign-in-page.link.create-an-account"
>
{i18n('auth.createAnAccount')}
</MaterialLink>
</OtherActions>
<I18nFlags style={{ marginTop: '24px' }} />
</form>
</FormProvider>
</Content>
</Wrapper>
);
}
. . .
Note that in the case of the InputFormItem, a composite Material-UI component,
we need to pass a new property PageName. This requires an adjustment to be
made to the component itself, as shown below:
export function InputFormItem(props) {
const {
label,
name,
hint,
type,
placeholder,
autoFocus,
autoComplete,
required,
externalErrorMessage,
disabled,
endAdornment,
pageName,
} = props;
. . .
return (
<TextField
id={name}
name={name}
type={type}
label=
{<div data-test-id={"label." + {name}.name} data-trans-key={pageName + ".label." +
name}>{label}</div>}
required={required}
12. 12
inputRef={register}
onChange={(event) => {
props.onChange &&
props.onChange(event.target.value);
}}
onBlur={(event) => {
props.onBlur && props.onBlur(event);
}}
margin="normal"
fullWidth
variant="outlined"
size="small"
placeholder={placeholder || undefined}
autoFocus={autoFocus || undefined}
autoComplete={autoComplete || undefined}
InputLabelProps={{
shrink: true,
}}
error={Boolean(errorMessage)}
helperText=
{<div data-test-id={"helper-text."+ {name}.name} data-trans-key={pageName + ".helper-text." +
name}>{errorMessage || hint}</div>}
InputProps={{
endAdornment,
inputProps: {
"data-test-id": "textField."+ {name}.name,
}
}}
disabled={disabled}
/>
);
}
. . .
InputFormItem.propTypes = {
name: PropTypes.string.isRequired,
required: PropTypes.bool,
type: PropTypes.string,
label: PropTypes.string,
hint: PropTypes.string,
autoFocus: PropTypes.bool,
disabled: PropTypes.bool,
prefix: PropTypes.string,
placeholder: PropTypes.string,
autoComplete: PropTypes.string,
externalErrorMessage: PropTypes.string,
onChange: PropTypes.func,
endAdornment: PropTypes.any,
pageName: PropTypes.string,
};
. . .
With the above changes we can locate the individual inner elements of
InputFormItem as well as retrieve the translation-related attribute, data-trans-
key, which will allow us to independently retrieve the expected text of the
corresponding element in any of the target spoken languages. The details of
this retrieval mechanism will be fully described in later sections.
13. 13
THE ROLE OF BDD
From the outset, BDD was architected to describe business behaviours,
business workflow performed by a user of an application. The use of BDD to
express a programmatic, procedural, flow is not in keeping with this objective.
If a procedural style of test is felt to be more the way forward then JUnit (here),
NUnit (here) or TestNG (here) is perhaps more appropriate.
Honouring the intention of BDD means that it is the driver for what happens in
the test, the associated Step Definitions and any supporting code are required
to do the heavy lifting to achieve the required, assertive, outcome of the overall
test.
In recent times an alternative view of BDD has emerged that might be termed
Technical BDD. This is exemplified in the Karate framework (here). A later
article will look at, and compare, the “classic” and “technical” BDD approaches
(here).
TRANSLATION MECHANISM
As well as the adaption of the React code, with the addition of custom attributes
whose value represents a key to translation data sources, we need two further
items to complete our architecture, a class which manages the retrieval of
expected textual matter given a specific key, and a translation data source.
TRANSLATOR CLASS
To allow structured access to our translation data, we use the class
PageTextTranslator, a fragment of which is shown below:
. . .
/**
* This class represents a project-level text translation service.
*/
public class PageTextTranslator {
private String spokenLanguageMnemonic;
public static final String NAME_RESOURCE_PACKAGE =
"environments/translation";
public static final String NAME_RESOURCE_FILE_NAME =
"PageTranslations.json";
public static final String LIST_SEPARATION_CHARACTER = "|";
private static final String LIST_SPLIT_CHARACTER = "|";
private static final String REPLACE_STRING_OLD = ".";
private static final String REPLACE_STRING_NEW = "/";
private PageTextTranslator() {}
14. 14
public PageTextTranslator( String langMnemonic ) {
this.spokenLanguageMnemonic = langMnemonic;
}
/**
* Get the appropriate textual value for a specified key
* @param keyPath = the JSON key path to the value
* @return the textual value for the specified key in
* the current specified spoken language.
*/
public String getTranslation( String keyPath ) {
String translationText = "";
Reader reader = null;
try {
String filePath = UtilsFile.getResourceAbsoluteFilePath(
NAME_RESOURCE_PACKAGE,
NAME_RESOURCE_FILE_NAME );
reader = Files.newBufferedReader(new File(filePath).toPath());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode parser = objectMapper.readTree(reader);
translationText =
parser.at( finaliseKeyPath(keyPath)).textValue();
} catch ( java.io.IOException ignored) {}
finally {
if ( reader != null ) {
try {
reader.close();
} catch ( java.io.IOException ignored ) {}
}
}
return (translationText == null ) ?
Constants.MISSING_VALUE : translationText;
}
private String finaliseKeyPath( String suppliedPartialKey ) {
return "/" +
suppliedPartialKey.replaceAll( REPLACE_STRING_OLD,
REPLACE_STRING_NEW) +
"/" +
spokenLanguageMnemonic;
}
. . .
}
Note that the translation file is expected to be in the resources section of the
project, e.g. (…> resources > environments > translation) and the file itself be
named PageTranslations.json. In addition, the eventual key into the data is a
composite of the key provided with the current spoken language mnemonic
appended (finaliseKeyPath(…)). So, for example, if the provided key was
“sign-in-page.link.forgot-password”, and the current language mnemonic is
“ES”, then the composite key into the datastore would be “sign-in-
page.link.forgot-password.ES”.
In our example the translation datastore is represented as a JSON file, the
details of which we present in the next section.
It should be borne in mind that the translation key could be one relevant for a
central, enterprise, location, in which case out translator class would need to
change.
15. 15
TRANSLATION DATA
When the visual elements in the front-end code are modified to carry the new
custom attribute, allowing for page-unique referencing of textual elements, then
the corresponding translation datastore (PageTranslations.json) looks as
shown in the fragment below:
{
"sign-in-page": {
"label": {
"title": {
"EN": "Application",
"ES": "Aplicación",
"BR": "Aplicação"
},
"email": {
"EN": "Email",
"ES": "Email",
"BR": "Email"
},
"password": {
"EN": "Password",
"ES": "Contraseña",
"BR": "Senha"
}
},
"helper-text": {
"email": {
"EN": "Email is required",
"ES": "Email es obligatorio",
"BR": "Email é obrigatório"
},
"password": {
"EN": "Password is required",
"ES": "Contraseña es obligatorio",
"BR": "Senha é obrigatório"
}
},
"link": {
"remember-me": {
"EN": "Remember me",
"ES": "Recuérdame",
"BR": "Lembrar-me"
},
"forgot-password": {
"EN": "Forgot password",
"ES": "Se te olvidó tu contraseña",
"BR": "Esqueci minha senha"
},
. . .
16. 16
WORKFLOW EXAMPLE
With the previously described pieces in place, we are now able to describe the
validating tests we want to perform.
BDD STATEMENTS
For the use case “New User Sign-Up”, our BDD statements look like:
As can be seen, the Scenario Outline has an associated Examples table which
defines language-specific test cases, “EN”, “ES” and “BR”.
Several special features need to be described. Firstly, there is the annotations
for the Features as well as the Scenario. In practice, these would be references
to requirement id’s or perhaps a defect id, if, in the case of the Scenario, we
were describing a regression test workflow. The Scenario annotations have a
unique postfix number appended, e.g. “@REACT-MATERIAL_UI-2000.n”.
Secondly, the individual statements in the Scenario have their textual matter
prefixed by a Scenario-unique id, e.g. “[2000.n]”. This change frees us from the
Cucumber default behaviour of associating statements to Step Definitions only
by the matching of textual matter. Now we are able to write boxed,
maintainable Step Definitions which also allow data sharing as required by the
17. 17
Scenario, not limited by fears that the step is used somewhere else due to
default textual matching considerations (textual tyranny).
Thirdly, we use a definitional statement (“* def …”) as available in the Karate
(here) framework to construct a random prefix for our “sign up” email. It should
also be noted here, that at sign-up time the user does not get an email
confirmation, so we only need to supply an email address which has a valid
format. Our need to make it unique is so that, if we fail to delete the email in
the backend database for some reason (in our clean-up @After hook method),
later tests will run without clashing with email addresses that are already
registered in the system. We will look at the need to “tidy up” after a test in a
following section. This special statement cannot be drawn from the Karate
framework itself since “classic” Cucumber and Karate cannot be executed
together. However, the “*” prefix is available in “classic” Cucumber, so we need
only to implement our own Step Definition. This implementation is as shown
following:
package com.application.cucumber;
import io.cucumber.java.en.Given;
import java.util.Random;
/**
* This class contains static methods used to resolve Karate-type Cucumber statements.
*/
public class DefSteps {
private TestContextJava testContextJava;
public DefSteps(TestContextJava context ) {
testContextJava = context;
}
@Given("^def (w+) = random number between (d+) and (d+), with (d+)
decimals$")
public void def(String name, int lowInt, int highInt, int decimalPlaces) {
Random r = new Random();
int randomNumber = r.nextInt((highInt - lowInt) + 1) + lowInt;
testContextJava.put(name, randomNumber);
}
. . .
}
PAGE OBJECTS
The pages we saw above, in the section “The Application”, as noted there, are
expressed in our pattern of solution as Page Object classes. These are
generated, in fact, from metadata descriptions of the individual application page
– validations, actions etc.(Se34 (here))
A fragment of the Page Object for the SignInPage is shown below:
18. 18
package Se34.React.MaterialUI.Application.Java.Selenide;
import Se34.React.MaterialUI.Application.Java.Selenide.Support.BaseOnlineUIPage;
import Util.Constants;
import Util.PageTextTranslator;
import com.application.cucumber.TestContextJava;
import com.codeborne.selenide.Selectors;
import com.codeborne.selenide.Selenide;
import org.junit.Assert;
import org.openqa.selenium.By;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import static Util.TestParameters.BASE_URL;
import static com.codeborne.selenide.Selenide.$;
/**
* <p>
* This class represents the SignInPage of the web application.
* Created by the Se34 generation tool on 26/03/2021 15:25
* <p>
* {@code
* <auto-generated>
* <b>This code was generated by a tool. Runtime version:2.5.0</b>
* <b>Changes to this file may cause incorrect behaviour and will be lost if code is
regenerated.</b>
* </auto-generated>
* }
* </p>
* </p>
*/
public class SignInPage extends BaseOnlineUIPage {
private BigInteger _validationSet = new BigInteger("0");
private final String CUSTOM_ATTRIBUTE_LOCATION = "data-test-id";
private final String CUSTOM_ATTRIBUTE_TRANSLATION = "data-trans-key";
private final String PAGE_NAME = "SignInPage";
private TestContextJava _testContext;
private String _langMnemonic;
private PageTextTranslator _pageTextTranslator;
private BigInteger _errorMask;
private String _pageUrl = BASE_URL + "auth/signin";
public SignInPage(TestContextJava testContext, String langMnemonic ) {
this._testContext = testContext;
this._langMnemonic = langMnemonic;
this._pageTextTranslator = new PageTextTranslator(langMnemonic);
this._errorMask = Constants.BIG_INTEGER_ZERO;
}
public void clearErrorMask() {
this._errorMask = Constants.BIG_INTEGER_ZERO;
}
public void setLanguage( String langMnemonic ) {
this._langMnemonic = langMnemonic;
this._pageTextTranslator = new PageTextTranslator(langMnemonic);
}
// Components
public boolean hasComponents() { return false; }
// ---------------------------------------------------
// Overrides
public SignInPage waitFor() {
waitForPageLoadToComplete();
return this;
}
19. 19
public SignInPage navigate() {
Selenide.open( this._pageUrl );
Selenide.executeJavaScript("return document.readyState").toString().equals("complete");
return this;
}
// ---------------------------------------------------
// Declare the validation set
private String ApplicationHeaderLabelLocatorString = "label.title";
private BigInteger ValidateApplicationHeaderLabel = new BigInteger("1");
private String EmailLabelLocatorString = "label.email";
private BigInteger ValidateEmailLabel = new BigInteger("2");
. . .
private String EmailTextBoxLocatorString = "textField.email";
private String PasswordTextBoxLocatorString = "textField.password";
private String RememberMeCheckBoxLocatorString = "check-box.remember-me";
private String SignInButtonLocatorString = "button.sign-in";
private String EnglishLanguageIconLocatorString = "English.language.icon";
private String SpanishLanguageIconLocatorString = "Español.language.icon";
private String PortugueseLanguageIconLocatorString = "Português.language.icon";
// The Validator indicator functions
public SignInPage validateApplicationHeaderLabel() {
_validationSet = _validationSet.or(ValidateApplicationHeaderLabel);
return this;
}
public SignInPage validateEmailLabel() {
_validationSet = _validationSet.or(ValidateEmailLabel);
return this;
}
. . .
// Perform the appropriate non-dynamic validations
public BigInteger validatePage() {
_errorMask = Constants.BIG_INTEGER_ZERO;
if (
(_validationSet.and(ValidateApplicationHeaderLabel)).compareTo(ValidateApplicationHeaderLabel) == 0 )
{
String translationKey =
$(getApplicationHeaderLabelLocator()).getAttribute(CUSTOM_ATTRIBUTE_TRANSLATION);
Assert.assertNotNull("ApplicationHeaderLabel element is missing the
CUSTOM_ATTRIBUTE_TRANSLATION attribute", translationKey);
String expected = this._pageTextTranslator.getTranslation(translationKey);
if (expected.equals(Constants.MISSING_VALUE) ) {
_errorMask = _errorMask.or(ValidateApplicationHeaderLabel);
} else {
if ( !($(getApplicationHeaderLabelLocator()).getText().equals(expected)) ) {
_errorMask = _errorMask.or(ValidateApplicationHeaderLabel);
}
}
}
if ( (_validationSet.and(ValidateEmailLabel)).compareTo(ValidateEmailLabel) == 0) {
String translationKey =
$(getEmailLabelLocator()).getAttribute(CUSTOM_ATTRIBUTE_TRANSLATION);
Assert.assertNotNull("EmailLabel element is missing the CUSTOM_ATTRIBUTE_TRANSLATION
attribute", translationKey);
String expected = this._pageTextTranslator.getTranslation(translationKey);
if (expected.equals(Constants.MISSING_VALUE) ) {
_errorMask = _errorMask.or(ValidateEmailLabel);
} else {
if ( !($(getEmailLabelLocator()).getText().equals(expected)) ) {
_errorMask = _errorMask.or(ValidateEmailLabel);
}
}
}
20. 20
. . .
if (
(_validationSet.and(ValidateButtonSignInEnabled)).compareTo(ValidateButtonSignInEnabled) == 0 ) {
if ($(getButtonSignInTextLocator()).isDisplayed()) {
_errorMask = ($(getButtonSignInTextLocator()).isEnabled()) ? _errorMask :
_errorMask.or(ValidateButtonSignInEnabled);
} else {
_errorMask = _errorMask.or(ValidateButtonSignInEnabled);
}
}
if (
(_validationSet.and(ValidateEmailHelperTextVisible)).compareTo(ValidateEmailHelperTextVisible) == 0 )
{
_errorMask = ($(getEmailHelperTextLocator()).isDisplayed()) ? _errorMask :
_errorMask.or(ValidateEmailHelperTextVisible);
}
if (
(_validationSet.and(ValidatePasswordHelperTextVisible)).compareTo(ValidatePasswordHelperTextVisible)
== 0 ) {
_errorMask = ($(getPasswordHelperTextLocator()).isDisplayed()) ? _errorMask :
_errorMask.or(ValidatePasswordHelperTextVisible);
}
return _errorMask;
}
// Perform the appropriate dynamic validations
. . .
// The validation locators
public By getApplicationHeaderLabelLocator() {
return Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,ApplicationHeaderLabelLocatorString );
}
public By getEmailLabelLocator() {
return Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,EmailLabelLocatorString );
}
. . .
// The Page actions
public void doEmailValueEnter( String data ) {
$(Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,EmailTextBoxLocatorString )).isDisplayed();
$(Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,EmailTextBoxLocatorString )).isEnabled();
$(Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,EmailTextBoxLocatorString
)).setValue(data);
}
. . .
public void doRememberMeCheckBoxClick() {
$(Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,RememberMeCheckBoxLocatorString
)).isDisplayed();
$(Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,RememberMeCheckBoxLocatorString
)).isEnabled();
$(Selectors.byAttribute(CUSTOM_ATTRIBUTE_LOCATION,RememberMeCheckBoxLocatorString )).click();
}
. . .
// Page-specific Helper methods
public List<String> getErrorElementNames() {
List<String> errorNameList = new ArrayList<>();
if ( this._errorMask != Constants.BIG_INTEGER_ZERO) {
if (
_errorMask.and(ValidateApplicationHeaderLabel).compareTo(ValidateApplicationHeaderLabel) == 0) {
errorNameList.add("ApplicationHeaderLabel");
}
if ( _errorMask.and(ValidateEmailLabel).compareTo(ValidateEmailLabel) == 0 ) {
errorNameList.add("EmailLabel");
}
. . .
return errorNameList;
}
}
21. 21
Several key observations can be made regarding this generated Page Object:
• The naming convention of identifiers and methods is based on the root
name of the page or associated element,
• The structure of the code is very rhythmic and simple,
• The Page Object contains all the locator information of page elements,
• The appropriate validations and operations are “built-in” and use our text
translation class,
• The Page Object can be thought of, and is used in test code, as a service
provider
In the highlighted parts of validatePage(…), it can be seen how the translation
custom attribute is retrieved from the located SelenideElement (using the
custom attribute data-test-id and handling the case where it is missing).
Following, a call is made to the pageTextTranslator using the key just retrieved,
and it the value corresponding to this key that is then compared with the actual
displayed value, setting the overall validation “error bit mask” as appropriate.
It is not uncommon to be faced with an application element that need special
handling, whether in the validation or operation step. Se34 allows for these
cases in that a Page Object-external method is generated to which the
“standard” internal method delegates to satisfy the required operation.
STEP DEFINITIONS
The step definitions associated with the SignUp use case are written in Java (in
contrast to the earlier work, “UI Testing Pattern” (here), that used Kotlin
(here)) using the Selenide (here) framework to interact with the target
application.
The scenario-specific step definition code for SignUp is as shown below:
package com.application.cucumber;
import Se34.React.MaterialUI.Application.Java.Selenide.CreateWorkspacePage;
import Se34.React.MaterialUI.Application.Java.Selenide.DashboardPage;
import Se34.React.MaterialUI.Application.Java.Selenide.SignInPage;
import Se34.React.MaterialUI.Application.Java.Selenide.SignUpPage;
import Util.TestParameters;
import Util.UtilsFile;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.junit.Assert;
import java.math.BigInteger;
import java.util.List;
import static Util.Utils.outputErrorList;
22. 22
import static Util.UtilsDatabase.countUsers;
import static com.mysql.cj.Constants.BIG_INTEGER_ZERO;
public class SignUpSignInJourneySteps_2000_1 {
private TestContextJava testContextJava;
private static final String DEFAULT_LANGUAGE = "EN";
public SignUpSignInJourneySteps_2000_1(TestContextJava context ) {
testContextJava = context;
}
@Given("[2000.1] I navigate to the main landing page")
public void iNavigateToTheMainLandingPage() {
// Use default language
SignInPage signInPage = new SignInPage(testContextJava, DEFAULT_LANGUAGE);
testContextJava.put("sign-in-page", signInPage);
signInPage.navigate();
}
@And("[2000.1] I am a new user I want to Sign-Up in the {string} language")
public void iAmANewUserIWantToSignUpInTheLangLanguage( String langMnemonic) {
testContextJava.put("language-mnemonic", langMnemonic);
SignInPage signInPage = (SignInPage)testContextJava.get("sign-in-page", null);
Assert.assertNotNull("Did not find the SignInPage in the TestContext", signInPage);
// Update the language for the SignInPage
signInPage.setLanguage( langMnemonic );
}
@When("[2000.1] I select the appropriate language icon")
public void iSelectTheAppropriateLanguageIcon() {
String langMnemonic = (String)testContextJava.get("language-mnemonic", "EN");
SignInPage signInPage = (SignInPage)testContextJava.get("sign-in-page", null);
Assert.assertNotNull("Did not find the SignInPage in the TestContext", signInPage);
// we need to take care of the DOM adjustment that happens
// when Helper text gets displayed. Here we force the display
// of such text before we interact with items lower on the panel
signInPage.doEmailTextBoxClick();
signInPage.doApplicationLabelClick();
signInPage.doPasswordTextBoxClick();
signInPage.doApplicationLabelClick();
switch ( langMnemonic ) {
case "EN":
signInPage.doEnglishLanguageIconClick();
break;
case "ES":
signInPage.doSpanishLanguageIconClick();
break;
case "BR":
signInPage.doPortugueseLanguageIconClick();
break;
}
signInPage.waitForPageLoadToComplete();
try {
Thread.sleep(1000);
} catch (InterruptedException ignored ) {}
}
@Then("[2000.1] The SignIn page is shown in my chosen language")
public void theSignInPageIsShownInMyChosenLanguage() {
SignInPage signInPage = (SignInPage)testContextJava.get("sign-in-page", null);
Assert.assertNotNull("Did not find the SignInPage in the TestContext", signInPage);
// TODO we can validate the helper texts if we click in the fields
BigInteger errorMask = signInPage.
23. 23
validateRememberMeLabel().
validatePasswordLabel().
validateForgotPasswordLink().
validateEmailLabel().
validateButtonSignInText().
validateButtonSignUpEnabled().
validateApplicationHeaderLabel().
validatePage();
if (errorMask.compareTo(BIG_INTEGER_ZERO) != 0 ) {
List<String> errorList = signInPage.getErrorElementNames();
outputErrorList( "signInPage", errorList);
}
Assert.assertTrue("The SignInPage failed validation",
errorMask.compareTo(BIG_INTEGER_ZERO) == 0);
signInPage.clearErrorMask();
}
@When("[2000.1] I select account creation link")
public void iSelectAccountCreationLink() {
String langMnemonic = (String)testContextJava.get("language-mnemonic", "EN");
SignInPage signInPage = (SignInPage)testContextJava.get("sign-in-page", null);
Assert.assertNotNull("Did not find the SignInPage in the TestContext", signInPage);
// we need to take care of the DOM adjustment that happens
// when Helper text gets displayed. Here we force the display
// of such text before we interact with items lower on the panel
signInPage.doEmailTextBoxClick();
signInPage.doApplicationLabelClick();
signInPage.doPasswordTextBoxClick();
signInPage.doApplicationLabelClick();
signInPage.doCreateAccountLinkClick();
SignUpPage signUpPage = new SignUpPage(testContextJava, langMnemonic);
testContextJava.put("sign-up-page", signUpPage);
signUpPage.waitForPageLoadToComplete();
// TODO the navigation from the SingInPage to the SignUpPage
// seems to be sometimes unreliable. How can we improve this?
try {
Thread.sleep(1000);
} catch (InterruptedException ignored ) {}
}
@Then("[2000.1] The SignUp page is shown in my chosen language")
public void theSignUpPageIsShownInMyChosenLanguage() {
SignUpPage signUpPage = (SignUpPage)testContextJava.get("sign-up-page", null);
// TODO we can validate the helper texts if we click in the fields
BigInteger errorMask = signUpPage.
validateApplicationHeaderLabel().
validateButtonSignUpText().
validatePage();
if (errorMask.compareTo(BIG_INTEGER_ZERO) != 0 ) {
List<String> errorList = signUpPage.getErrorElementNames();
outputErrorList( "SignUpPage", errorList);
}
Assert.assertTrue("The SignUpPage failed validation",
errorMask.compareTo(BIG_INTEGER_ZERO) == 0);
signUpPage.clearErrorMask();
}
@When("[2000.1] I enter valid credentials {string} and {string} and {string} and {string}")
public void iEnterValidCredentialsEmailPrefixAnd$RandomIntegerAndEmailPostfixAndPassword(
String emailPrefix,
String randomInt,
String emailPostfix,
String password ) {
SignUpPage signUpPage = (SignUpPage)testContextJava.get("sign-up-page", null);
Assert.assertNotNull("The SignUpPage was null", signUpPage);
// TODO check that the data is valid and non-empty
24. 24
// TODO we should check that this is a valid email address when composed
// Before this step we have "def"d a random value
int randomValue = (int)testContextJava.get(UtilsFile.getVariableName(randomInt), null);
Assert.assertNotNull("Failed to retrieve the random integer", randomInt);
String emailString = emailPrefix.trim() + randomValue + emailPostfix.trim();
// we need to take care of the DOM adjustment that happens
// when Helper text gets displayed. Here we force the display
// of such text before we interact with items lower on the panel
signUpPage.doEmailTextBoxClick();
signUpPage.doApplicationLabelClick();
signUpPage.doPasswordTextBoxClick();
signUpPage.doApplicationLabelClick();
signUpPage.doEmailValueEnter(emailString);
signUpPage.doPasswordValueEnter(password.trim());
testContextJava.put( "user-email", emailString );
}
@And("[2000.1] I select Sign Up button")
public void iSelectSignUpButton() throws InterruptedException {
String langMnemonic = (String)testContextJava.get("language-mnemonic", "EN");
SignUpPage signUpPage = (SignUpPage)testContextJava.get("sign-up-page", null);
Assert.assertNotNull("The SignUpPage was null", signUpPage);
BigInteger errorMask = signUpPage.
validateButtonSignUpEnabled().
validatePage();
if (errorMask.compareTo(BIG_INTEGER_ZERO) != 0 ) {
List<String> errorList = signUpPage.getErrorElementNames();
outputErrorList( "SignUpPage", errorList);
}
Assert.assertTrue("The SignUpPage failed validation",
errorMask.compareTo(BIG_INTEGER_ZERO) == 0);
signUpPage.clearErrorMask();
// Check count of users in d/b
int userCountInitial = countUsers( "" );
signUpPage.doSignUpButtonClick();
CreateWorkspacePage createWorkspacePage = new CreateWorkspacePage(testContextJava, langMnemonic);
testContextJava.put("create-workspace-page", createWorkspacePage);
createWorkspacePage.waitForPageLoadToComplete();
// Give a time span to completion
Thread.sleep(TestParameters.DATABASE_ADD_TIME_DELAY_MS);
// Check here that the users in the d/b have incremented by 1
int userCountFinal = countUsers( "" );
System.out.println( "User Count initial: " + userCountInitial + "; user Count Final: " +
userCountFinal );
Assert.assertTrue("The user count was not as expected", userCountFinal == (userCountInitial + 1));
// The user is actually created at this point
testContextJava.put("user-created", true );
}
@Then("[2000.1] The Create Workspace page is shown in my chosen language")
public void theCreateWorkspacePageIsShownInMyChosenLanguage() {
CreateWorkspacePage createWorkspacePage = (CreateWorkspacePage)testContextJava.get("create-workspace-
page", null);
Assert.assertNotNull("The CreateWorkspacePage was null", createWorkspacePage);
BigInteger errorMask = createWorkspacePage.
validateApplicationHeaderLabel().
validateButtonCreateEnabled().
validateSignOutLink().
25. 25
validateWorkspaceNameLabel().
validatePage();
if (errorMask.compareTo(BIG_INTEGER_ZERO) != 0 ) {
List<String> errorList = createWorkspacePage.getErrorElementNames();
outputErrorList("CreateWorkspacePage", errorList);
}
Assert.assertTrue("The CreateWorkspacePage failed validation",
errorMask.compareTo(BIG_INTEGER_ZERO) == 0);
createWorkspacePage.clearErrorMask();
}
@When("[2000.1] I enter a valid {string} name")
public void iEnterAValidWorkspaceName( String workspaceName ) {
CreateWorkspacePage createWorkspacePage = (CreateWorkspacePage)testContextJava.get("create-workspace-
page", null);
Assert.assertNotNull("The CreateWorkspacePage was null", createWorkspacePage);
// we need to take care of the DOM adjustment that happens
// when Helper text gets displayed. Here we force the display
// of such text before we interact with items lower on the panel
createWorkspacePage.doWorkspaceNameTextBoxClick();
createWorkspacePage.doApplicationLabelClick();
createWorkspacePage.doWorkspaceNameValueEnter( workspaceName );
}
@And("[2000.1] I select Create Workspace")
public void iSelectCreateWorkspace() {
CreateWorkspacePage createWorkspacePage = (CreateWorkspacePage)testContextJava.get("create-workspace-
page", null);
Assert.assertNotNull("The CreateWorkspacePage was null", createWorkspacePage);
BigInteger errorMask = createWorkspacePage.
validateButtonCreateEnabled().
validatePage();
if (errorMask.compareTo(BIG_INTEGER_ZERO) != 0 ) {
List<String> errorList = createWorkspacePage.getErrorElementNames();
outputErrorList( "CreateWorkspacePage", errorList);
}
Assert.assertTrue("The CreateWorkspacePage failed validation",
errorMask.compareTo(BIG_INTEGER_ZERO) == 0);
createWorkspacePage.clearErrorMask();
createWorkspacePage.doCreateButtonClick();
}
@Then("[2000.1] The Dashboard page is shown in my chosen language")
public void theDashboardPageIsShownInMyChosenLanguage() {
String langMnemonic = (String)testContextJava.get("language-mnemonic", "EN");
DashboardPage dashboardPage = new DashboardPage( testContextJava, langMnemonic);
dashboardPage.waitFor();
}
}
It should be noted that in the Step Definition code there is a distinct lack of
locator-type information, this being safely held in the individual Page Objects.
Because we have introduced the “[…]” prefix in our BDD statements, the above
Step Definition methods now operate as a boxed set of methods which
correspond only to the correspondingly annotated set of statements. We can
extend, modify of data-share as we wish over time without causing side effects
anywhere else in our testing codebase. A good outcome.
26. 26
HOOKS
Because in the SignUp process of our application, data gets written to a back-
end database, we need to take care to clean-up after test execution. To this end
we need to have suitable logic in an @After hook method. The class containing
this is shown below:
package com.application.cucumber;
import Util.TestParameters;
import Util.UtilsDatabase;
import com.codeborne.selenide.Configuration;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import org.junit.Assert;
import java.io.IOException;
import static Util.UtilsDatabase.countUsers;
import static com.codeborne.selenide.Selenide.closeWebDriver;
public class SignUpSignInJourneySteps_2000 {
private TestContextJava testContextJava;
private static final String DEFAULT_LANGUAGE = "EN";
public SignUpSignInJourneySteps_2000(TestContextJava context ) {
testContextJava = context;
}
@Before
public void setup() {
// Configure the browser
// Implicit & page load timeouts
Configuration.browser = "chrome";
Configuration.timeout = 60000;
Configuration.pageLoadStrategy = "eager";
Configuration.pageLoadTimeout = 60000;
Configuration.startMaximized = true;
testContextJava.put("user-created", false );
}
@After
public void cleanUp() throws InterruptedException {
closeWebDriver();
String email = (String)testContextJava.get( "user-email", "" );
// If we need to clean up the database, this is where we perform that action
if ( (boolean)testContextJava.get("user-created", false) ) {
Assert.assertTrue( "Failed to delete user in the database",
UtilsDatabase.deleteUser( email ) );
Thread.sleep(TestParameters.DATABASE_ADD_TIME_DELAY_MS);
// Check that a users selection with specific email
// results in a result set count of 0
int userCount = countUsers( "email='" + email + "'" );
Assert.assertTrue("The user count for a specific email was not as expected [" +
userCount + "]", userCount == 0 );
}
27. 27
// Kill any zombie Chrome processes (if we are running the Chrome browser)
try {
Runtime.getRuntime().exec("taskkill /F /IM chrome.exe /T");
}catch ( IOException unhandled ) {}
testContextJava.clearTestContextCache();
}
}
The key to our clean-up is the retrieval and testing of the boolean value “user-
created”. At the start of a Scenario test execution this value is set to false in
the testContextJava cache, and when the user is actually created (after clicking
the SignUp button) we set its value to true. Note the Page Object does not
contain state variables.
In the case where we detect that a new user has in fact been added, signed up,
then we use utility methods to both delete the user as well as check the email-
specific user count in the database to validate the deletion. The
testContextJava cache is cleared irrespective of user deletion.
EXECUTION
Executing our Feature from within the IntelliJ IDE, we see:
Summary
The foregoing has presented an architecture for handling textual translation
using Java and Selenide. This architecture has at its centre the adaption of
visual elements in the React code to add a custom translation key (“data-trans-
key”). In addition, the needed classes and JSON file support for this translation
28. 28
approach were introduced. As described in our earlier article (here) we also add
a custom attribute which enables reliable location, (“data-test-id”).
The translation key could be one that points to a central translation data store
as can be available in the enterprise. In this case the class PageTextTranslator
would need to be refactored to allow access to such a central place.
The primacy of the BDD statements was noted as was a novel approach to
break the default textual based linkage between these statements and their
associated Step Definitions. The value of such an approach, in terms of boxing
the Step Definitions and thus significantly benefiting both maintenance as well
as fluent data sharing over the steps of a Scenario, was highlighted.
For on-project, enterprise-level test automation, the role of Page Object
generation from meta-data cannot be overstated – speed, embracing change,
maintenance.