-
Appium
-
Selenium
-
HttpClient
-
TestNG
-
Extent Reports
-
Parallel Execution of Android or iOS devices
-
Api + Web automation
-
Page object Model using a single screen class to store both android and ios locators at same place.
-
Common gestures and controls like Full Screen Swipe, Half Screen swipe, Pickerwheel interactions, Keyboard handling, Bring element to focus, scroll, click, enter text, etc.
-
Execution configuration for local and cloud devices as well as application environment via single yaml file.
-
Log4j for logging.
-
Extent Reports.
-
Retry for failed cases (In Progress)
Every Module/Screen Test Cases you will create using this framework will have following 4 things (Example provided below) -
-
Each Screen Class will have static fields to store both android & ios locators of ProjectMobileBy type.
-
-
Contains utility methods for micro-interaction steps needed to perform on the app. Like "clickOnXXX", "getTitle", etc.
-
Each Test Steps Class must have a constructor which intializes
AppiumLibrary
object with the driver passed in argument
-
-
-
This will contain abstracted navigation flows to be used in your test automation and assertions.
-
Each method in the class will contain multiple interactions utility methods created in Steps class to be reused while writing tests across different modules.
-
-
-
This is the main Test Class for each functionality/suite. This will contain your
@Test
methods and is responsible for calling the test code (defined in Flows class). -
Each and every Test Class must extend BaseClass which will provide driver and other session related fields. And override
initializeTestObjects()
method which will intialize helper objects to be used in the test case.
-
We are going to see an example of a common login test case. I will attach code snippets from the framework style and then explain what it does below them.
public final static ProjectMobileBy SIGN_IN_WITH_EMAIL_BTN = new ProjectMobileBy(MobileBy.id("ib_login_email"), MobileBy.xpath("//*[contains(@name, 'emailButton')]"));
public final static ProjectMobileBy USER_NAME_FIELD = new ProjectMobileBy(MobileBy.xpath("//android.widget.EditText[contains(@resource-id,'editText')]"), MobileBy.xpath("//*[contains(@name,'emailTextField')]"));
Here you will define fields of type ProjectMobileBy which accepts 2 MobileBy as arguments, one for android and other for ios.
ProjectMobileBy
is a special class in the framework, object of which holds together both os locators for the common element. It will supply the appropriate MobileBy
locator to AppiumDriver
based on execution os automatically on the go.
Step 2 : Create a Test Steps Class - LoginTestSteps.java
, extending LoginScreen
class created in Step 1
public class LoginSteps extends LoginScreen
{
AppiumLibrary appiumLibrary;
UserOnboardingDetailsSteps userOnboardingDetailsSteps;
public LoginSteps(SessionManager sessionManager) {
appiumLibrary = new AppiumLibrary(sessionManager);
}
public void clickOnLoginWithEmailButton() {
appiumLibrary.clickOnMobileElement(SIGN_IN_WITH_EMAIL_BTN);
}
public void enterUserName(String userName) {
appiumLibrary.enterText(USER_NAME_FIELD, userName);
}
public void enterPassword(String password) {
appiumLibrary.enterText(PASSWORD_FIELD, password);
}
public void clickOnLoginButton() {
appiumLibrary.clickOnMobileElement(LOGIN_BTN);
}
}
As you can see, it contains micro-interaction utility methods using the locator objects you created in Step 1. In the example, its trying to perform 4 different steps -
-
Click on Login with email option (to land on login screen)
-
Enter username
-
Enter password
-
Click on login button
You might have noticed the constructor contains 2 classes -
SessionManager
- This class manages the appium session across the test suite. For every steps, flow class we will create we must supply this object in the constructor while creating their object. It will be supplied via BaseClass
. (More on this later)
AppiumLibrary
- Contains appium interaction methods to actually interact with app on mentioned element locators.
Every Steps class MUST have the constructor mentioned. In addition it can include other Steps objects if you want to use them. More on them later.
So, once we have all the interaction methods in Steps Class, we would want a wrapper around them to combine those unit steps into meaningful user-flows.
This Flows Class will contain all such methods which ofcourse will comprise of calling multiple "steps".
For example, one "flow" will be to login successfully. We have already defined 4 steps above, lets define a wrapper flow method to combine the above to create a flow -
public class LoginFlow extends LoginSteps {
private LogUtility logUtility = new LogUtility(LoginFlow.class);
public LoginFlow(SessionManager sessionManager) {
super(sessionManager);
logUtility.setExtentLogger(sessionManager.getExtentReporter().getExtentlogger());
}
public void loginWithEmail(String email, String passowrd) {
clickOnLoginWithEmailButton();
enterUserName(email);
enterPassword(passowrd);
clickOnLoginButton();
}
}
As you can see, we have created a similar constructor for LoginFlow
class as we did for LoginSteps
. We created a method loginWithEmail
method to log in a user. You can include an additional assertion to verify whether the login is successful but you get the idea.
So, now we have to finally add our TestNG class to put everything together. We create a LoginTest
class which can contain multiple @Test
methods for each of our test case.
public class LoginTest extends BaseClass {
private LoginFlow loginFlow;
private LogUtility logUtility = new LogUtility(LoginTest.class);
@Override
public void initializeTestObjects() {
loginFlow = new LoginFlow(sessionManager);
logUtility.setExtentLogger(sessionManager.getExtentReporter().getExtentlogger());
}
@Test
public void loginTest() {
logUtility.logTestTitle("Login Test");
logUtility.logTestInfo("Checking login flow");
loginFlow.loginSuccessfully();
//other flows
}
}
The Test Class will extend BaseClass
from where we are gonna receive an instance of SessionManager
containing initialized AppiumDriver
(as per the configuration specified).
You notice, we have initializeTestObjects
method which overrides it for the one in parent BaseClass
. This is necessary to assign SessionManager
instance (initialized in @BeforeTest) to pass into your flows classes.
And then we have our @Test
method to call all the flows we want to and log the steps and other info along the way.
You will have to add a TestConfiguration.yaml file where you will add all the configuration details & appium installation paths for framework to pick up. Add a file which looks like this in main project directory -
testEnvironment: staging
baseUrl: http://devapi.mycubii.com
qaBaseUrl: http://devapi.mycubii.com
platformOs: android
isTestRunOnSpecificDevice: false
executionDevicesList:
- Samsung Galaxy S8
- Redmi 5
android:
name: Google Pixel2 XL emulator
udid: emulator-5554
version: 9.0
ios:
name: iPhone 8
udid: 81A58605-90E2-4936-80EE-447124220D91
version: 13.3
androidPackageName:
iosPackageName:
executionEnvironment: local
allowAPIUtilsOnProdEnvironment: false
nodePath: /usr/local/bin/node
appiumPath: /usr/local/lib/node_modules/appium/build/lib/main.js
-
You would use appium interaction utility methods defined in
AppiumLibrary
in your Steps class. It supportsclickOnElement
,clickOnElementIfPresent
,getAttribute
,getText
,scrollToElement
,bringElementToTop
,hideKeyboard
,pressKey
,enterText
,scrollUp
,scrollDown
,swipeOnElement
and lot more actions! -
Many times in an application, some steps and flows are common for all/most parts of the app. For example, menu navigation panel could remain same no matter the screen user is on. Or pressing on notifications icon which is present on many/all screens of the app. So it makes sense to write interaction methods for these in a CommonFlow class and use it in any of your module Flows class.
-
For every class, declare a variable of
LogUtility
and in constructor, (As seen in examples above) initialize by passing instance of extent logger from sessionmanager like this -
@Override
public void initializeTestObjects() {
loginFlow = new LoginFlow(sessionManager);
logUtility.setExtentLogger(sessionManager.getExtentReporter().getExtentlogger());
}
It contains various helpful logging methods to log on different levels as well as different markups in the TestNG and Extent Reports like -
logUtility.logTestTitle("Testing Login functionality");
logUtility.logStep("Step " + (++n) + " : Clicking on login button");
logUtility.logWarning("Element not found, trying again.");
logUtility.logTestInfo("Current device is " + sessionManager.getCurrentDevice());
-
Similar to AppiumLibrary, WebLibrary contains interaction utility methods for Selenium like click, enterText, switchToWindow, scrollUp, scrollDown and many more!
-
Classes with public final static fields to store project directory paths, test data like user credentials, etc. Anything you might need to access globally in your tests!
-
You can find methods related to installing/uninstalling apps on your phone, some adb, idevice utility commands [Not finished yet]
-
DBUtility
class establishes a MySQL connection whenever its constructor is called. Set your dbUrl, username and password insrc/main/resources/{environment}.properties
file. To execute query -
DBUtility dbUtility = new DBUtility();
dbUtility.executeQuery(your_query);
dbUtility.closeDbConnection();
-
Normally you would store your api requests in json files. This class will help you read those files to json objects or arrays -
JSONObject loginRequest = JSONFileUtility.getJsonFileAsJsonObject(LoginRequest.json);
Well, that's okay you ask but how to actually call an API?
We have written a wrapper around HttpClient called ApiUtility
. Below is an example to get a json object from a file, replace variables in the json with their actual intended values in the code and then send a post request and then retrieve a value from the response and return it -
Lets say you have loginRequest.json(in apiRequests folder) with {{email}}
and {{password}}
as variables -
{
"email" : "{{email}}",
"password" : "{{password}}"
}
Calling login api to login with passed email & password -
public String loginWithEmailViaApi(User user) {
String baseUrl = TestConfiguration.getBaseUrl() + "/api/v5/login/";
Map<String, String> headers = apiUtility.getCommonHeaders(); //You can define whatever common headers like Content-Type = application/json
//get the json file
JSONObject requestBody = JSONFileUtility.getJsonFileAsJsonObject(Constants.API_REQUESTS_PATH + Constants.slash + "loginRequest.json");
//prepare a map for the variables you defined in your json file and their values
Map<String, Object> variablesMap = new HashMap<>();
variablesMap.put("email", user.getEmail());
variablesMap.put("password", user.getPassword());
//now prepare the final json body by replacing all the values of variables
JSONObject finalBody = JSONFileUtility.replaceVariablesInJsonWithTheirValuesFromMap(requestBody, variablesMap);
//call the api and convert the HttpResponse object to JSONObject
JSONObject my_api_response_json = apiUtility.convertResponseToJsonObject(apiUtility.sendRequest("post", baseUrl, headers, finalBody));
//retrieve token from the response and return it
JSONObject dataJson = (JSONObject) my_api_response_json.get("data");
String token = dataJson.get("token").toString();
String userId = dataJson.get("id").toString();
user.setToken(token);
user.setUserId(userId);
return token;
}
Similarly, you can call your get, put apis with whatever arguments you want to. Simple, right?
Run on android(or ios) on whatever devices connected -
mvn clean test -DisTestRunOnSpecificDevice = false -DplatformOs = android (or ios) -DtestSuites=Login,Ocb -DtestEnvironment=staging
Run on android(or ios) on specific devices connected -
mvn clean test -DisTestRunOnSpecificDevice = true -DplatformOs = android (or ios) -DexecutionDevicesList = "Samsung Galaxy J6, Nokia 3" -DtestSuites=Login,Ocb -DtestEnvironment=staging
Running via testng.xml is exactly how it should be - Just add a testng xml file with your tests & classes and you are good to go.
Detailed Explanation & Architecture Diagram coming soon...
- How to configure default reports in IntelliJ - Go to Run -> Edit Congurations.
Under TestNG configuration, go to Listeners tab.
Add org.testng.reporters.TestHTMLReporter listener and save it.
Click on Use default reporters.