SlideShare a Scribd company logo
1 of 28
Download to read offline
BABBLE
BDD/React/Material-UI/Java/Language Translation
D. Harrison
March 2021
Babble v. to utter sounds or words imperfectly, indistinctly, or without meaning.
© 2021, David Harrison, All Rights Reserved
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
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
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
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
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.
7
Invalid/unrecognised credentials case.
SignUpPage
Helper text displayed case.
This panel will be displayed in the language selected on the initial SignIn page
(see above).
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
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
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
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
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
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
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
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
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
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
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
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
. . .
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
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
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
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
// 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
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
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
// 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
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.

More Related Content

What's hot

5 tsssisu sql_server_2012
5 tsssisu sql_server_20125 tsssisu sql_server_2012
5 tsssisu sql_server_2012
Steve Xu
 
Create a balanced scorecard
Create a balanced scorecardCreate a balanced scorecard
Create a balanced scorecard
Steve Xu
 
Tutorial%20fivestar%20cck%20views
Tutorial%20fivestar%20cck%20viewsTutorial%20fivestar%20cck%20views
Tutorial%20fivestar%20cck%20views
tutorialsruby
 
Sap ps module tutorial
Sap ps module tutorialSap ps module tutorial
Sap ps module tutorial
achyuth10
 
Jug Guice Presentation
Jug Guice PresentationJug Guice Presentation
Jug Guice Presentation
Dmitry Buzdin
 
People soft workflow by surya 2
People soft workflow by surya 2People soft workflow by surya 2
People soft workflow by surya 2
meghamystic
 
Type Adoption in xCP 2.1 Applications
Type Adoption in xCP 2.1 ApplicationsType Adoption in xCP 2.1 Applications
Type Adoption in xCP 2.1 Applications
Haytham Ghandour
 
User and group security migration
User and group security migrationUser and group security migration
User and group security migration
Amit Sharma
 

What's hot (18)

5 tsssisu sql_server_2012
5 tsssisu sql_server_20125 tsssisu sql_server_2012
5 tsssisu sql_server_2012
 
Create a balanced scorecard
Create a balanced scorecardCreate a balanced scorecard
Create a balanced scorecard
 
Hibernate III
Hibernate IIIHibernate III
Hibernate III
 
Dependency Injection
Dependency InjectionDependency Injection
Dependency Injection
 
JSP Technology II
JSP Technology IIJSP Technology II
JSP Technology II
 
Test script
Test scriptTest script
Test script
 
Validation type 'special' in value sets
Validation type 'special' in value setsValidation type 'special' in value sets
Validation type 'special' in value sets
 
Tutorial%20fivestar%20cck%20views
Tutorial%20fivestar%20cck%20viewsTutorial%20fivestar%20cck%20views
Tutorial%20fivestar%20cck%20views
 
Sap ps module tutorial
Sap ps module tutorialSap ps module tutorial
Sap ps module tutorial
 
Jug Guice Presentation
Jug Guice PresentationJug Guice Presentation
Jug Guice Presentation
 
People soft workflow by surya 2
People soft workflow by surya 2People soft workflow by surya 2
People soft workflow by surya 2
 
Oracle Fusion Role Mappings
Oracle Fusion Role MappingsOracle Fusion Role Mappings
Oracle Fusion Role Mappings
 
Java 14 support in Eclipse IDE
Java 14 support in Eclipse IDEJava 14 support in Eclipse IDE
Java 14 support in Eclipse IDE
 
Type Adoption in xCP 2.1 Applications
Type Adoption in xCP 2.1 ApplicationsType Adoption in xCP 2.1 Applications
Type Adoption in xCP 2.1 Applications
 
User and group security migration
User and group security migrationUser and group security migration
User and group security migration
 
Using Page Objects
Using Page ObjectsUsing Page Objects
Using Page Objects
 
Outlook autodiscover decision process choosing the right autodiscover method ...
Outlook autodiscover decision process choosing the right autodiscover method ...Outlook autodiscover decision process choosing the right autodiscover method ...
Outlook autodiscover decision process choosing the right autodiscover method ...
 
e-KSF Process Sheets
e-KSF Process Sheetse-KSF Process Sheets
e-KSF Process Sheets
 

Similar to Babble article - Test Automation & Text Translation

CucumberSeleniumWD
CucumberSeleniumWDCucumberSeleniumWD
CucumberSeleniumWD
Vikas Sarin
 
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
MartineMccracken314
 
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
AbbyWhyte974
 
Define and Manage Requirements with IBM Rational Requirements Composer
Define and Manage Requirements with IBM Rational Requirements ComposerDefine and Manage Requirements with IBM Rational Requirements Composer
Define and Manage Requirements with IBM Rational Requirements Composer
Alan Kan
 
Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...
Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...
Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...
guest96f6c68d
 

Similar to Babble article - Test Automation & Text Translation (20)

IntoTheNebulaArticle.pdf
IntoTheNebulaArticle.pdfIntoTheNebulaArticle.pdf
IntoTheNebulaArticle.pdf
 
IntoTheNebulaArticle.pdf
IntoTheNebulaArticle.pdfIntoTheNebulaArticle.pdf
IntoTheNebulaArticle.pdf
 
CucumberSeleniumWD
CucumberSeleniumWDCucumberSeleniumWD
CucumberSeleniumWD
 
Behaviour Driven Development V 0.1
Behaviour Driven Development V 0.1Behaviour Driven Development V 0.1
Behaviour Driven Development V 0.1
 
PagesToGo.pdf
PagesToGo.pdfPagesToGo.pdf
PagesToGo.pdf
 
Em wp
Em wpEm wp
Em wp
 
PhoenixRisingArticle.pdf
PhoenixRisingArticle.pdfPhoenixRisingArticle.pdf
PhoenixRisingArticle.pdf
 
PhoenixRisingArticle.pdf
PhoenixRisingArticle.pdfPhoenixRisingArticle.pdf
PhoenixRisingArticle.pdf
 
PHPConf.asia 2016 - BDD with Behat for Beginners
PHPConf.asia 2016 - BDD with Behat for BeginnersPHPConf.asia 2016 - BDD with Behat for Beginners
PHPConf.asia 2016 - BDD with Behat for Beginners
 
Visual Studio and Xamarin: The future of app development
Visual Studio and Xamarin: The future of app developmentVisual Studio and Xamarin: The future of app development
Visual Studio and Xamarin: The future of app development
 
what is context API and How it works in React.pptx
what is context API and How it works in React.pptxwhat is context API and How it works in React.pptx
what is context API and How it works in React.pptx
 
Zend con 2016 bdd with behat for beginners
Zend con 2016   bdd with behat for beginnersZend con 2016   bdd with behat for beginners
Zend con 2016 bdd with behat for beginners
 
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
 
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
1. After reviewing chapters 9, 10, and 11 of the Kotler and Kell
 
Define and Manage Requirements with IBM Rational Requirements Composer
Define and Manage Requirements with IBM Rational Requirements ComposerDefine and Manage Requirements with IBM Rational Requirements Composer
Define and Manage Requirements with IBM Rational Requirements Composer
 
Robert polak matrix skills-web developer 2018-3
Robert polak   matrix skills-web developer 2018-3Robert polak   matrix skills-web developer 2018-3
Robert polak matrix skills-web developer 2018-3
 
Installing community surveys in connections 5.5
Installing community surveys in connections 5.5Installing community surveys in connections 5.5
Installing community surveys in connections 5.5
 
Automation test framework with cucumber – BDD
Automation test framework with cucumber – BDDAutomation test framework with cucumber – BDD
Automation test framework with cucumber – BDD
 
Introduction to Behavior Driven Development
Introduction to Behavior Driven Development Introduction to Behavior Driven Development
Introduction to Behavior Driven Development
 
Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...
Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...
Developing Dynamic PeopleSoft Field Security Applications:A PeopleSoft Develo...
 

More from David Harrison

More from David Harrison (11)

SchemaStudioTypeLandscape_Article.pdf
SchemaStudioTypeLandscape_Article.pdfSchemaStudioTypeLandscape_Article.pdf
SchemaStudioTypeLandscape_Article.pdf
 
Processor Refactoring.pdf
Processor Refactoring.pdfProcessor Refactoring.pdf
Processor Refactoring.pdf
 
Generation_XSD_Article - Part 4.pdf
Generation_XSD_Article - Part 4.pdfGeneration_XSD_Article - Part 4.pdf
Generation_XSD_Article - Part 4.pdf
 
Generation_XSD_Article - Part 3.pdf
Generation_XSD_Article - Part 3.pdfGeneration_XSD_Article - Part 3.pdf
Generation_XSD_Article - Part 3.pdf
 
Generation_XSD_Article - Part 2.pdf
Generation_XSD_Article - Part 2.pdfGeneration_XSD_Article - Part 2.pdf
Generation_XSD_Article - Part 2.pdf
 
Generation_XSD_Article.docx
Generation_XSD_Article.docxGeneration_XSD_Article.docx
Generation_XSD_Article.docx
 
High sierra part 1
High sierra part 1High sierra part 1
High sierra part 1
 
Selenium Testing @ Agile Speed
Selenium Testing @ Agile SpeedSelenium Testing @ Agile Speed
Selenium Testing @ Agile Speed
 
Workflow Test Automation
Workflow Test AutomationWorkflow Test Automation
Workflow Test Automation
 
Et sensus agile documentation
Et sensus   agile documentationEt sensus   agile documentation
Et sensus agile documentation
 
Web Test Automation
Web Test AutomationWeb Test Automation
Web Test Automation
 

Recently uploaded

6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...
6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...
6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...
@Chandigarh #call #Girls 9053900678 @Call #Girls in @Punjab 9053900678
 
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
nilamkumrai
 
📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱
📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱
📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱
@Chandigarh #call #Girls 9053900678 @Call #Girls in @Punjab 9053900678
 
💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋
💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋
💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋
nirzagarg
 
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
Call Girls In Delhi Whatsup 9873940964 Enjoy Unlimited Pleasure
 
VIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 Booking
VIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 BookingVIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 Booking
VIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 Booking
dharasingh5698
 

Recently uploaded (20)

6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...
6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...
6.High Profile Call Girls In Punjab +919053900678 Punjab Call GirlHigh Profil...
 
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
 
20240508 QFM014 Elixir Reading List April 2024.pdf
20240508 QFM014 Elixir Reading List April 2024.pdf20240508 QFM014 Elixir Reading List April 2024.pdf
20240508 QFM014 Elixir Reading List April 2024.pdf
 
APNIC Updates presented by Paul Wilson at ARIN 53
APNIC Updates presented by Paul Wilson at ARIN 53APNIC Updates presented by Paul Wilson at ARIN 53
APNIC Updates presented by Paul Wilson at ARIN 53
 
APNIC Policy Roundup, presented by Sunny Chendi at the 5th ICANN APAC-TWNIC E...
APNIC Policy Roundup, presented by Sunny Chendi at the 5th ICANN APAC-TWNIC E...APNIC Policy Roundup, presented by Sunny Chendi at the 5th ICANN APAC-TWNIC E...
APNIC Policy Roundup, presented by Sunny Chendi at the 5th ICANN APAC-TWNIC E...
 
Russian Call Girls Pune (Adult Only) 8005736733 Escort Service 24x7 Cash Pay...
Russian Call Girls Pune  (Adult Only) 8005736733 Escort Service 24x7 Cash Pay...Russian Call Girls Pune  (Adult Only) 8005736733 Escort Service 24x7 Cash Pay...
Russian Call Girls Pune (Adult Only) 8005736733 Escort Service 24x7 Cash Pay...
 
All Time Service Available Call Girls Mg Road 👌 ⏭️ 6378878445
All Time Service Available Call Girls Mg Road 👌 ⏭️ 6378878445All Time Service Available Call Girls Mg Road 👌 ⏭️ 6378878445
All Time Service Available Call Girls Mg Road 👌 ⏭️ 6378878445
 
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
 
Sarola * Female Escorts Service in Pune | 8005736733 Independent Escorts & Da...
Sarola * Female Escorts Service in Pune | 8005736733 Independent Escorts & Da...Sarola * Female Escorts Service in Pune | 8005736733 Independent Escorts & Da...
Sarola * Female Escorts Service in Pune | 8005736733 Independent Escorts & Da...
 
📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱
📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱
📱Dehradun Call Girls Service 📱☎️ +91'905,3900,678 ☎️📱 Call Girls In Dehradun 📱
 
20240509 QFM015 Engineering Leadership Reading List April 2024.pdf
20240509 QFM015 Engineering Leadership Reading List April 2024.pdf20240509 QFM015 Engineering Leadership Reading List April 2024.pdf
20240509 QFM015 Engineering Leadership Reading List April 2024.pdf
 
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
 
Pune Airport ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready...
Pune Airport ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready...Pune Airport ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready...
Pune Airport ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready...
 
💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋
💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋
💚😋 Salem Escort Service Call Girls, 9352852248 ₹5000 To 25K With AC💚😋
 
Dubai=Desi Dubai Call Girls O525547819 Outdoor Call Girls Dubai
Dubai=Desi Dubai Call Girls O525547819 Outdoor Call Girls DubaiDubai=Desi Dubai Call Girls O525547819 Outdoor Call Girls Dubai
Dubai=Desi Dubai Call Girls O525547819 Outdoor Call Girls Dubai
 
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
 
Busty Desi⚡Call Girls in Vasundhara Ghaziabad >༒8448380779 Escort Service
Busty Desi⚡Call Girls in Vasundhara Ghaziabad >༒8448380779 Escort ServiceBusty Desi⚡Call Girls in Vasundhara Ghaziabad >༒8448380779 Escort Service
Busty Desi⚡Call Girls in Vasundhara Ghaziabad >༒8448380779 Escort Service
 
VIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 Booking
VIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 BookingVIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 Booking
VIP Call Girls Himatnagar 7001035870 Whatsapp Number, 24/07 Booking
 
Ganeshkhind ! Call Girls Pune - 450+ Call Girl Cash Payment 8005736733 Neha T...
Ganeshkhind ! Call Girls Pune - 450+ Call Girl Cash Payment 8005736733 Neha T...Ganeshkhind ! Call Girls Pune - 450+ Call Girl Cash Payment 8005736733 Neha T...
Ganeshkhind ! Call Girls Pune - 450+ Call Girl Cash Payment 8005736733 Neha T...
 
Story Board.pptxrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
Story Board.pptxrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrStory Board.pptxrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
Story Board.pptxrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
 

Babble article - Test Automation & Text Translation

  • 1. BABBLE BDD/React/Material-UI/Java/Language Translation D. Harrison March 2021 Babble v. to utter sounds or words imperfectly, indistinctly, or without meaning. © 2021, David Harrison, All Rights Reserved
  • 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.
  • 7. 7 Invalid/unrecognised credentials case. SignUpPage Helper text displayed case. This panel will be displayed in the language selected on the initial SignIn page (see above).
  • 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.