application-root-dir/ web/ settings.gradle build.gradle test/ src/test/ java/ com/example/test/ MyInstanceTypeTest.java MyInstanceTest.java resources/ test.properties test-automation/ webdriver/ fileupload/ <module_name>/ build.gradleIf running tests on host without docker, put web drivers under the directory test/test-automation/webdriver/.
Web Driver Executables Driver Types Windows Linux/Mac ------------------------------------------------------- chrome chromedriver.exe chromedriver firefox geckodriver.exe geckodriver edge edgedriver.exe edgedriver sarafi safaridriver.exe safaridriver opera operadriver.exe operadriverThe fileupload directory is the place to put files for upload.
include 'test' project(':test').projectDir = new File('../test')test project build.gradle
apply from : "${rootProject.cmobilecomAFBuildCommonDir}/test.gradle"Run tests:
gradlew :test:test [options]options:
Whether to run tests remotely? e.g. Selenium Grid. Default is false.
Remote test server URL. default is http://localhost:4444 for Selenium Grid.
Whether to keep page loader(browser for web) after each test? Default is true.
The locale(language tag) for loading pages. e.g., en-US, default: en.
Override default theme if defined. Theme names are defined in <cmobilecom.home>/conf/themes.xml.
A list of driver types(separated by comma). Driver types: chrome, firefox, edge, safari, and opera. Default: chrome.
Wait time in seconds for expected conditions. default is 20.
Extra wait time after a wait condition and before sending next request. Requests might be ignored by browser without the extra wait time. default is 100.
Wait time in seconds for print dialog to load preview. Default is 4.
Window size: max, fullscreen, normal. Default is max.
Whether to enable assertions? If false, all the assert methods of test API will be ignored. Default is true.
A list of instanceType id(s) for which secure redirect is enabled. "all" stands for all instanceTypes. Default: none.
The user number which will be used as username suffix. Default is the next available user number.
Rerun a previous test using existing instances (userNo must be specified to identify the previous test).
The starting execute point for re-running a previous test. Default: from beginning.
Exec points are defined by module tests. Format:
<instanceIdentifier>.<module>.<module_exec_point> <instanceTypeId>.<module>.<module_exec_point>
Whether to enable test client JVM debugging? Default is false.
Test client JVM debugging listen host. e.g., localhost, 0.0.0.0 or *(listen to all addresses).
Test client JVM debugging listen port. Default is 5005.
Test filter for selecting the tests to run. For example, --tests *.testMyApp[0]
Enable test client JVM debugging, overriding -Pdebug.jvm.
@RunWith(Parameterized.class)
public class HRTest extends InstanceTypeTest {
public HRTest(TestConfig testConfig) {
super(testConfig);
}
@Override
public String getInstanceTypeId() {
return "hr";
}
@Override
protected InstanceTest createInstanceTest(InstanceIdentifier instanceIdentifier, ManageCenter manageCenter) {
return new HRInstanceTest(this, instanceIdentifier, manageCenter);
}
@Test
public void testHR() {
Locale mainUserLocale = getLocale(InstanceIdentifier.Main);
prepare();
// register user (instance creator) for main instance
registerUser(UserIdentifier.Main, true, mainUserLocale, InstanceIdentifier.Main);
// register regular user
registerUser(userEmployeeOne, false, mainUserLocale, null);
HRInstanceTest mainInstanceTest = (HRInstanceTest) loginOrCreateInstance(
InstanceIdentifier.Main, null, this.pageResource, false);
mainInstanceTest.run();
}
}
The example HR InstanceType test above registers two users, create the main instance and then
login to run the main instance test. Each instance is assigned a unique InstanceIdentifier,
and each user is assigned a unique UserIdentifier. InstanceIdentifier.MAIN is predefined for
the main instance, and UserIdentifier.Main is predefined for the main user.
Override the following method to provide profile for each user, which is used to fill out User Registration form. For example,
public static UserIdentifier userEmployeeOne = UserIdentifier.valueOf("EmployeeOne");
@Override
public UserProfile getUserProfile(UserIdentifier userIdentifier) {
String usernamePrefix = getUsernamePrefix(userIdentifier);
String name = userIdentifier.getName() + " Test User";
String username = Configurations.getNextUsername(usernamePrefix, false);
String email = username + "@" + Configurations.getDomain(getInstanceTypeId());
String password = Configurations.getInstancePassword();
return new UserProfile(name, username, email, password);
}
private String getUsernamePrefix(UserIdentifier userIdentifier) {
String usernamePrefix;
if (userIdentifier == UserIdentifier.Main)
usernamePrefix = "main-user-";
else if (userIdentifier == userEmployeeOne)
usernamePrefix = "emp1-user-";
else
throw new IllegalArgumentException("unknown user identifier: " + userIdentifier);
return usernamePrefix;
}
Each time when an InstanceType test is run, new users will be created. A username consists of
prefix and number: usernamePrefix-userNo. If userNo is not specified, the test will
search user table to find the next available user number. Override the following method to
specify the username prefix for the query.
@Override
protected String getUsernamePrefixForAvailableUserNumber() {
return "main-user-"
}
Login to an existing instance for re-running a previous test or create a new instance.
For example,
HRInstanceTest mainInstanceTest = (HRInstanceTest) loginOrCreateInstance(
InstanceIdentifier.Main, null, this.pageResource, false);
The MAIN user is registered as the creator of the MAIN instance.
registerUser(UserIdentifier.Main, true, mainUserLocale, InstanceIdentifier.Main);
So if the instance created by the MAIN user is not found, a new instance will be created.
public class HRInstanceTest extends InstanceTest {
public HRInstanceTest(InstanceTypeTest instanceTypeTest,
InstanceIdentifier instanceIdentifier, ManageCenter manageCenter) {
super(instanceTypeTest, instanceIdentifier, manageCenter);
}
@Override
protected List<Class<? extends ModuleTest>> getModuleTests() {
return Arrays.asList(SystemTest.class, HRModuleTest.class);
}
}
An InstanceTest needs to override getModuleTest() method to provide the list of
ModuleTest classes. The order is the run order of module tests.
By default, An InstanceTest will call run() method of all module tests, which then call
run(ExecPointRange) method. Override the following method to change this default behavior.
For example,
@Override
protected void runModuleTest(ModuleTest moduleTest) {
if (moduleTest instanceof SystemTest) {
SystemTest systemTest = (SystemTest) moduleTest;
Parameters parameters = getParameters();
systemTest.setParameters(parameters);
return true;
}
super.runModuleTest(moduleTest);
}
moduleRootDir/src main/ test/ java/com/example/test/MyModuleTest.java resources/bundle/ messages*.propertiesModule test jars will be built and added to the compilation and runtime classpath of InstanceType tests.
If one module test(say Module "A") has dependency on another module test(say Module "B"), add the following dependencies in the build.gradle of module A.
dependencies { testImplementation project(path: ':module-b', configuration: 'moduleTestJar') }Module main jar will be added to the runtime classpath of InstanceType tests, which include module resource bundles. A module test must extend ModuleTest, define ExecPoint enum type and specify the ExecPoint type as actual generic type parameter. For example,
public class HRModuleTest extends ModuleTest<ExecPoint> {
public enum ExecPoint {
create_employees,
create_expenseClaims,
show_expenseClaims,
verify_expenseReport,
change_theme_and_locale,
batchPrint_expenseClaims
}
public HRModuleTest(InstanceTest instanceTest) {
super(instanceTest);
}
@Override
public String getModuleName() {
return "HR";
}
@Override
public void run(ExecPointRange execPointRange) {
// init timeZone and currencyCode
ManageCenter manageCenter = getManageCenter();
manageCenter.getTimeZone();
manageCenter.getCurrencyCode();
// run the exec points within the range
for (int i = execPointRange.getStart().ordinal(); i <= execPointRange.getEnd().ordinal(); i++) {
ExecPoint execPoint = ExecPoint.values()[i];
if (execPoint == ExecPoint.create_employees)
createEmployees();
else if (execPoint == ExecPoint.create_expenseClaims)
createExpenseClaims();
else if (execPoint == ExecPoint.show_expenseClaims)
showExpenseClaims();
else if (execPoint == ExecPoint.verify_expenseReport)
verifyExpenseReport();
else if (execPoint == ExecPoint.change_theme_and_locale)
changeThemeAndLocale();
else if (execPoint == ExecPoint.batchPrint_expenseClaims)
batchPrint_expenseClaims();
}
}
The module name must be the same as the module name on server side, case-sensitive.
The ExecPointRange of the run() method is the range after -PgotoExec is applied. The ordinal values of ExecPoints is the run order. Module test run() method goes through all the ExecPoints in the range, and call corresponding methods.
public static IdRuleEntityType typeEmployee = IdRuleEntityType.valueOf("HR", "Employee");
public static IdRuleEntityType typeExpenseClaim = IdRuleEntityType.valueOf("HR", "ExpenseClaim");
@Override
protected Map<IdRuleEntityType, IdRule> getIdRules() {
IdRuleEntityType[] idRuleEntityTypes = new IdRuleEntityType[] {
typeEmployee, typeExpenseClaim
};
Map<IdRuleEntityType, IdRule> idRules = new HashMap<>();
for (IdRuleEntityType entityType : idRuleEntityTypes) {
int idLen = (entityType == typeEmployee)? 5 : 10;
IdRule idRule = new IdRule(entityType, InstanceTest.BarcodeType.CODE128, null, idLen,
"#{sn:" + idLen + "}", 1);
idRules.put(entityType, idRule);
}
return idRules;
}
Entity type names will be translated using the resource bundle of current user locale,
and they must match the entity type display names on server side. For example, entity
type Employee's name is Employee on both server side and test code.
TestClock testClock = moduleTest.getTestClock();
testClock.advanceDays(days, reset);
testClock.advanceHours(hours, reset);
testClock.advanceMillis(millis, reset);
testClock.advanceToNextMonth(timeZoneId, dayOfMonth);
minus values will adjust test clock backward. The parameter reset (boolean) indicates
whether to remove all previous adjustments. See javadoc for details.
Test clock can be taken a snapshot at a certain timestamp, and restored at later time.
testClock.takeSnapshot(name, millsToAdd);
// do something, then restore the clock
testClock.advanceToSnapshot(name, millisToAdvance);
The millsToAdd and millisToAdvance are adjustment to the snapshot.
If an InstanceType test needs to create entities with different timestamps across a number of days in current month, call the following method at the start of the test to adjust the test clock.
testClock.ensureDaysAvailableInCurrentMonth(timeZoneId, daysAvailable, hoursAvailableInDay);
For example, a test needs to create 10 Orders on 10 consecutive days in current month,
and today in Mar 25. So there is not enough days left on current month. Suppose it takes
less than 2 hours to run the test.
testClock.ensureDaysAvailableInCurrentMonth(timeZoneId, 10, 2);
The timeZoneId is the time zone of current instance test. The hour adjustment is to
make sure the test will not run to the next day for the time zone. Otherwise, days
available in current month can not be guaranteed.
One test clock for an InstanceType test and its instance tests. That is, if a module test adjusted test clock, it actually adjusted the test clock for all other module tests in the same InstanceType test. Note that server host clock is not affected, and test clock is not available for production.
Component --------------------------------------------------------------- | | | | Bean EntityProperty ContainerBean MenuNode / \ | MenuBean PersistenceDataBean DialogBean / \ EntityBean EntityListBeanTest component hierarchy and composition are the same as logical view API for server.
List<Region> regions = containerBean.getPageContent().getRegions();
Region region = containerBean.getPageContent().getRegion(index);
List<Bean> beanList = region.getBeanList();
Bean bean = region.getBean(index);
List<MenuNode> topMenuNodes = menuBean.getTopMenuNodes();
MenuNode menuNode = menuBean.getMenuNodeWithCommand(commandName);
MenuNode menuNode = menuBean.getMenuNode(String ... namePath);
MenuBean headerMenu = persistenceDataBean.getHeaderMenu();
MenuBean footerMenu = persistenceDataBean.getFooterMenu();
EntityProperty property = entityBean.getEntityProperty(propertyName);
String value = property.getValue(visibleText);
// get total number of entities
int entityCount = entityListBean.getEntityCount();
// get number of entities in current page
int pageEntityCount = entityListBean.getPageEntityCount();
// get page count
int pages = entityListBean.getPageCount();
// get current page number (1-based)
int pageNo = entityListBean.getCurrentPageNumber();
// get paginator menu
MenuBean paginagorMenu = entityListBean.getPaginatorMenu();
// get EntityProperty at a rowIndex
EntityProperty property = entityListBean.getEntityProperty(rowIndex, propertyName);
String value = property.getValue(visibleText);
// got rowCommand menu at a rowIndex
MenuBean rowCommandMenu = entityListBean.getRowCommandMenu(rowIndex);
DialogBean dialogBean = containerBean.waitUntilDialogOpen();
DialogBean childDialogBean = dialogBean.waitUntilDialogOpen();
EntityBean inputDataBean = menuNode.openInputDataBean();
MenuBean propertyMenu = entityProperty.getPropertyMenu();
EntityProperties and MenuNodes are leaf components for inputs and actions. For an EntityProperty, user(test) can input value, select single or multiple options, open dialog to select or create entities, click property value as command link, respond to captcha, etc. Property value change or action can trigger partial behavior events (send request to server).
For a MenuNode, user(test) can click menu node to trigger partial behavior events (send request to server), open its inputDataBean, open child menu nodes, etc.
The ways to get root ContainerBean:
// ModuleTest
ContainerBean containerBean = getManageCenter().getContainerBean();
// Load a page using PageResource
ContainerBean containerBean = instanceTypeTest.pageResource.getPage(dataAccessUnit, url, locale);
One PageResource is mapped to one driver instance (e.g., web browser instance).
To create a new PageResource (e.g., open a new web browser) to load pages:
PageResource pageResource = new PageResource(instanceTypeTest.getTestConfig());
// create pageLoader
pageResource.before();
ContainerBean containerBean = pageResource.getPage(dataAccessUnit, url, locale);
//EntityProperty:
// void setValue(String query);
employeeBean.getEntityProperty("name").setValue("Employee One" + "\t");
Special keys RETURN(\n) and TAB(\t) are supported at the end of input value,
which can trigger partial behavior events (action or value change).
//EntityProperty:
// void autoComplete(String query, int selectItemIndex);
// void autoComplete(String query, String pattern, boolean exactMatch, boolean visibleText);
expenseClaimBean.getEntityProperty("employee").autoComplete("123456", 0);
expenseClaimBean.getEntityProperty("employee").autoComplete("123", "Employee One", false, true);
//EntityProperty:
// void setChecked(boolean checked);
EntityProperty resetPasswordEP = parametersBean.getEntityProperty("resetPassword");
resetPasswordEP.setChecked(true);
//EntityProperty:
// void selectOption(String pattern, boolean exactMatch, boolean visibleText);
// void selectOptionNull();
addressBean.getEntityProperty("country").selectOption("US", true, false);
addressBean.getEntityProperty("country").selectOptionNull();
//EntityProperty:
// void selectOptions(List<String> patterns, boolean exactMatch, boolean visibleText);
// void selectAllOptions();
EntityProperty groupByEP = queryBean.getEntityProperty("groupByProperties");
groupByEP.selectOptions(Arrays.asList("employee", "code"), true, false);
//EntityProperty:
// void selectFiles(List<String> files);
EntityProperty uploadedFilesEP = fileUploadBean.getEntityProperty("uploadedFiles");
uploadedFilesEP.selectFiles(files);
MenuNode uploadFilesMenuNode = fileUploadBean.getFooterMenu().getMenuNodeWithCommand("Upload");
uploadFilesMenuNode.click();
Upload files as attachments of an entity. For example, upload all image files with a filter
under a directory as employee photos.
//EntityProperty:
// void uploadFiles(List<String> files);
// void uploadFiles(File dir, FilenameFilter filter);
FilenameFilter filenameFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.startsWith("employee-") && name.endsWith(".jpg");
}
};
EntityProperty photosEP = employeeBean.getEntityProperty("photos");
photosEP.uploadFiles(dir, filenameFilter);
//EntityProperty:
// DialogBean openInputDialog(boolean showCreateEntityForm);
EntityProperty userEP = employeeBean.getEntityProperty("user");
// select user
DialogBean userDialogBean = userEP.openInputDialog(false);
userDialogBean.selectEntity("username", "employee-user-1"); // dialog closed
//EntityProperty:
// DialogBean openInputDialog(boolean showCreateEntityForm);
EntityProperty departmentEP = employeeBean.getEntityProperty("department");
DialogBean deptDialogBean = departmentEP.openInputDialog(true);
EntityBean departmentBean = (EntityBean) deptDialogBean.getDefaultContentBean();
departmentBean.getEntityProperty("name").setValue(bundle.getString("HR"));
departmentBean.getEntityProperty("manager").setValue(bundle.getString("HRManager"));
departmentBean.create(false);
// select department
departmentBean.selectAsValueOf(departmentEP);
EntityProperty captchaProperty = userBean.getEntityCaptchaProperty(false);
if (captchaProperty != null)
captchaProperty.clickCaptcha();
//EntityListBean:
// void scanBarcode(String barcode);
EntityListBean orderItemListBean = (EntityListBean) orderBean.getFormBean("orderItems");
orderItemListBean.scanBarcode(barcode);
orderItemListBean.waitUntilStale(true);
When a bean is added to (or removed from) a Region, the region will become stale. For example,
MenuBean containerMenuBean = getContainerMenuBean();
Region defaultContentRegion = getDefaultContentRegion();
MenuNode employeesMenuNode = containerMenuBean.getMenuNode("HR", "Settings", "Employees");
employeesMenuNode.click();
EntityListBean employeeListBean = (EntityListBean) defaultContentRegion.waitUntilStale(true).getBean(0);
After clicking the menu node (HR > Settings > Employees), wait until the content region becomes stale and updated. Then get the rendered employee list bean.
The Component base class has method waitUntilStale(refresh). For example,
EntityListBean expenseClaimItemListBean = (EntityListBean) expenseClaimBean.getFormBean("expenseClaimItems");
EntityProperty statisticsExpenseEP = expenseClaimItemListBean.getStatisticsEntityProperty("expense");
expenseClaimItemListBean.getEntityProperty(0, "expense").setValue("1000\t");
statisticsExpenseEP.waitUntilStale(true);
statisticsExpenseEP.assertEquals(BigDecimal.valueOf(1000), currencyNumberFormat);
After entering the expense amount and press TAB key, the total expense will be updated.
component.waitUntilVisible() // wait until the component becomes visible
component.waitUntilInvisible() // wait until the component becomes invisible
component.waitUntilClickable() // wait until the component becomes clickable
For example,
MenuNode changeThemeMenuNode = pageHeaderMenuBean.getMenuNodeWithCommand("ChangeTheme");
changeThemeMenuNode.click();
EntityBean inputDataBean = changeThemeMenuNode.getInputDataBean();
inputDataBean.waitUntilVisible();
// dialog opened by server
DialogBean dialogBean = containerBean.waitUntilDialogOpen();
// dialog closed by server
containerBean.waitUntilDialogClose();
// e.g., create an entity in dialogBean
// dialog closed by user (test)
dialogBean.close();
If dialog is closed by user(test), do not need to explicitly call wait method.
A dialog is opened because of a request (e.g., click a menu node), so user(test)
can not open a dialog directly.
// click to open window
openWindowLink.click();
ContainerBean childWindowContainerBean = containerBean.switchToChildWindow();
// close and switch to parent window
childWindowContainerBean.closeWindow();
To wait util a child window is open or closed, pass the expected number of
windows after the child window is open or closed.
// numberOfWindows is the number of windows before opening child window
String windowHandle = containerBean.waitUntilWindowOpen(numberOfWindows + 1);
ContainerBean childWindowContainerBean = containerBean.switchToChildWindow(windowHandle);
// wait until child window to close and switch to parent window
childWindowContainerBean.waitUntilWindowClose(numberOfWindows);
EntityProperty nameEP = employeeBean.getEntityProperty("name");
nameEP = nameEP.waitUntilValueToBe(true/*waitUntilStale*/, "Hello World", true/*visibleText*/);
// toggle edit row
EntityProperty quantityEP = orderItemListBean.getEntityProperty(rowIndex, "quantity");
orderItemListBean.toggleRowEditMode(rowIndex);
// wait quantity property to become editable
quantityEP.waitUntilEditable(true/*refresh*/);
// wait quantity property to become not editable
quantityEP.waitUntilReadOnly(true/*refresh*/);
ContainerBean:
public MessageItem waitUntilMessageContains(int messageItemIndex, String text, boolean title);
public MessageItem waitUntilMessageMatches(int messageItemIndex, Pattern pattern, boolean title);
A containerBean has a MessagePanel that contains a number of MessageItem. Each MessageItem has message title and body.
For example,
MessageItem messageItem = containerBean.getMessagePanel().getMessageItem(1);
messageItem.close();
String currencyCode = manageCenter.getCurrencyCode();
NumberFormat currencyNumberFormat = orderBean.getCurrencyNumberFormat(currencyCode, 2);
EntityListBean orderItemListBean = (EntityListBean) orderBean.getFormBean("orderItems);
// assert the subTotal value of the 2nd order item
EntityProperty subTotalEP = orderItemListBean.getEntityProperty(1, "subTotal");
subTotalEP.assertEquals(BigDecimal.valueOf(1000), currencyNumberFormat);
// assert sum(subTotal) of all order items
EntityProperty totalEP = orderItemListBean.getStatisticsEntityProperty("subTotal");
totalEP.assertEquals(BigDecimal.valueOf(2000), currencyNumberFormat);
Both EntityBean and EntityListBean have convenient methods for asserting property values.
For example,
orderItemListBean.assertEquals(0, "subTotal", BigDecimal.valueOf(1000), currencyNumberFormat);
EntityListBean employeeListBean = containerBean.showEntityList("HR", "Settings", "Employees");
employeeListBean.assertEntityCount(100);
@RunWith(Parameterized.class)
public class HRTest extends InstanceTypeTest {
@Test
public void testHR() {
Locale mainUserLocale = getLocale(InstanceIdentifier.Main);
boolean rerunTest = isRerunTest();
if (!rerunTest) { // run tests from scratch
prepare();
// register user for main instance
registerUser(UserIdentifier.Main, true, mainUserLocale, InstanceIdentifier.Main);
registerUser(userEmployeeOne, false, mainUserLocale, null);
}
else { // use existing instances
addRegisteredUser(UserIdentifier.Main, InstanceIdentifier.Main);
addRegisteredUser(userEmployeeOne, null);
}
HRInstanceTest mainInstanceTest = (HRInstanceTest) loginOrCreateInstance(
InstanceIdentifier.Main, null, this.pageResource, false);
mainInstanceTest.run();
}
}
domain.system=system.Cmobilecom-AF-Examples.com
domain.hr=hr.Cmobilecom-AF-Examples.com
# subdomain=test/3
protocol=http
port.http=8080
port.https=8443
# contextPath: default [software-name]-[software-version]
contextPath=
system.username=system
system.password=123456
instance.password=Welcome123&
auth.twoStepAuth=true
# whether to open print preview dialog for printing test
printView.openPrintDialog=false
The contextPath for page URLs defaults to <software-name>-<software-version>
that are defined in build.properties. If contextPath is root, set it to empty.
domain.system = example-domain.com domain.app1 = app1.example-domain.com subdomain = test/3The domain example-domain.com will become test.example-domain.com, and the domain app1.example-domain.com will become test-app1.example-domain.com.
example-domain.com ---> test.example-domain.com app1.example-domain.com ---> test-app1.example-domain.comDefault is unlimited levels of subdomains. That is, if subdomain=test, then
example-domain.com ---> test.example-domain.com app1.example-domain.com ---> test.app1.example-domain.com
The instance password will be used as the password of the user registrations. It must follow the Password Policy set in conf/system-config.xml.