User manual

1. Each test class extends from AbstractTest

  • Test class extends tech.testnx.cah.AbstractTest
  • Implements method getModuleName()

2. Managed WebDriver

  • Get a default webdriver specified in global-config.properties

    WebDriver driver = getDriver();

  • Get a specific webdriver

    WebDriver driver = newChromeDriver();
    WebDriver driver = newFirefoxDriver();
    WebDriver driver = newIeDriver();

  • Customized the WebDriver by listeners WebDriverListeners

    WebDriver driver = getDriver();
    WebDriverEventListener listener= new AbstractWebDriverEventListener() {};
    driver = registerDriverListener(driver, listener);
    driver = unregisterDriverListener(driver, listener);

  • Managed the WebDrivers by pool DriverPool

    WebDriver driver1 = requestWebDriver();
    WebDriver driver2 = this.requestWebDriver(BrowserType.CHROME);
    WebDriver driver3 = this.requestWebDriver(BrowserType.FIREFOX);
    releaseWebDriver(driver1);
    releaseWebDriver(driver2);
    releaseWebDriver(driver3);

3. Managed RESTful API Client

  • Get a RestClient and RestClientV2
    RestClient

    RestClient client = RestClient.newInstance("http://endpoint/api/");
    RestfulResponse response = client.doGet();
    Assert.assertEquals(response.getStatus(), 200);

    RestClientV2 clientV2 = RestClientV2.newInstance();
    RestfulResponse response = clientV2.doGet("http://endpoint/api/", null);
    Assert.assertEquals(response.getJsonParser().getStringValue("status"), "success");
    Assert.assertEquals(response.getJsonParser().getIntValue("data[2].employee_age"), 65);

  • Customized the RestClient by listeners RestClientListeners

    RestClientV2 client = RestClientV2.newInstance();
    RestClientListener listener = new RestClientListener() {};
    client.registerListener(listener);
    client.unregisterListener(listener);

4. Configure the path of resources directory

CAH configs, data, drivers, logs, reports are stored in resources directory. Now it is possible to customize the path.

  • Set up Environment Variable: CAH_HOME=Path; the directory should contain resources directory
  • Create cah-home directory which should contain resources directory
  • Use current working directory with java code, which should contain resources directory

5. Test Data Management

  • Test data type supported: JSON and XLS

    • Test
      @Test
      @Description(testIDs = "T-005", 
      title = "Data Ready", 
      description = "Test plus: ${{1}} + ${{2}} = ${{3}}?")
      public void testDataPlus(String description, int first, int second, int result) {
      	Recorder.log(description);
      	Assert.assertEquals(first + second, result);
      }
      
      @Test
      @Description(testIDs = "T-006", 
      title = "Data Ready2", 
      description = "Test minus: ${{1}} - ${{2}} = ${{3}}?")
      public void testDataMinus(String description, int first, int second, int result) {
      	Recorder.log(description);
      	Assert.assertEquals(first - second, result);
      }
      
    • Json
      {
      "common": {
      	"commonData1": "value for commonData1"
      },
      "tests": {
      	"BasicTests": {
      		"testDataPlus": [
      			{
      				"description": "Verify the 5 + 5 = 10?",
      				"first": 5,
      				"second": 5,
      				"result": 10
      			}
      		],
      		"testDataMinus": [
      			{
      				"description": "Verify the 7 - 5 = 1?",
      				"first": 7,
      				"second": 5,
      				"result": 1
      			}
      		]
      	}
        }
      }
      
    • xls xls-data
  • Auto-binding between test and data with zero effort, support types: Boolean, Short, Integer, Long, Float, Double, String, JsonNode, TransformableTestDataBean(interface)

    • Test(JsonNode)

      @Test
      public void test3(String testname, JsonNode node) {
      	System.out.println(testname);
      	System.out.println(node.toString());
      	System.out.println(node.path("student").path(0).path("name").asText());
      }
      
    • Json(JsonNode)

      		"test3": [
      			{
      				"testname": "TESTCASE-3",
      				"node": {
      					"student": [
      						{
      							"id": 1,
      							"name": "Tom",
      							"isNew": true
      						},
      						{
      							"id": 2,
      							"name": "Nick",
      							"isNew": false
      						}
      					]
      				}
      			}
      		]
      
    • XLS(JsonNode) xls-JsonNode

    • Data Bean(TransformableTestDataBean)

       1   public class Test4DataBean implements TransformableTestDataBean<Test4DataBean> {
       2
       3   public int id;
       4   public String name;
       5   public boolean isNew;
       6
       7   @Override
       8   public Test4DataBean transformTestData(String content) {
       9
      10	    try {
      11	  	  return Utilities.jsonUtility.getMapper().readValue(content, Test4DataBean.class);
      12	    } catch (IOException e) {
      13	  	  Logger.getLogger().error("Failed to transform test data from: " + content);
      14	  	  e.printStackTrace();
      15	  	  throw new RuntimeException("Failed to transform test data");
      16	    }
      17    }
      18
      19    @Override
      20    public boolean equalsValue(Test4DataBean t) {
      21
      22  	  if (id != t.id) return false;
      23  	  if (!name.equals(t.name)) return false;
      24  	  if (isNew != t.isNew) return false;
      25  	  return true;
      26    }
      27    }
      
    • Test(TransformableTestDataBean)

        @Test
        public void test4(String testname, Test4DataBean bean) {
      	  System.out.println(testname);
      	  System.out.println(bean.id);
      	  System.out.println(bean.name);
      	  System.out.println(bean.isNew);
        }   
      
    • Json(TransformableTestDataBean)

      		"test4": [
      			{
      				"testname": "TESTCASE-4",
      				"Test4DataBean": {
      					"id": 1,
      					"name": "Tom",
      					"isNew": true
      				}
      			}
      		]
      
    • XLS(TransformableTestDataBean) xls-TransformableTestDataBean

  • Data driven test
    If one test has multiple data groups, the test will be automatically executed multiple times with different data group

    • Test
        @Test
        public void testSample(String a, String b){
          System.out.println(a + " :::: " + b);
          }
      
    • JSON
          "DataDrivenTests": {
          "testSample": [
            {
              "a": "my key",
              "b": "my value"
            },
            {
              "a": "your key",
              "b": "your value"
            }
          ]
      
    • XLS xls-datadriven
  • Auto-Decrypt secured test data Encrypted test data by Utilities.securityUtility.encrypt() and save them in data file, then the test data will be auto-decrypted for test
    secured-test-data

6. Share and manage the state across tests by StateManager

When user need share or pass some state or varible across tests, StateManager will provide help.

    @Test
    public void test1() {
      logger.info("Start");
    	StateManager.setState("test1.stateA", "good");
    	logger.info("End");
    }

    @Test
    public void test2() {
    	logger.info("Start");
        if (StateManager.getState("test1.stateA").equals("good")) {
            logger.info("Take action A");
            } else {
            logger.info("Take action B");
            }
    	logger.info("End");
    }

7. Template test

Template test could be useful in API test. the API payload with the same schema will be used in multiple tests and environments.
CAH framework provides two two Template Composer: CAH-Composer and Freemarker.
The template files could be stored in both module folder and ennvironment folder, but the template files are higher priority in ennvironment folder.

graph LR A[Project] --> B(resources) --> C(data) C --> json(json) --> module-a(module-A) C --> xls(xls) --> module-b(module-B) module-a --> env-a(env-A) --> templates-e-a(templates) module-a --> env-b(env-B) --> templates-e-b(templates) module-a --> templates-m-a(templates)

	@Test
	public void test_CAH_COMPOSER(String name, String age, String isStudent) {
		//Test CAH_COMPOSER
		Map<String, String> dataModel = new HashMap<>();
		dataModel.put("name", name);
		dataModel.put("age", age);
		dataModel.put("isStudent", isStudent);
		
		ComposeTemplate compoer = TemplateComposerProvider.CAH_COMPOSER.getComposer();
    //test1-templates.json could be store in both module folder and environment folder
		String content = compoer.compose(dataModel, dataClient.findTemplate("test1-templates.json").get().toFile());
		logger.info(content);
		JsonNode rootNode = Utilities.jsonUtility.readTree(content);
		CahAssert.assertEquals(rootNode.path("name").asText(), name);
		CahAssert.assertEquals(rootNode.path("age").asInt(), Integer.valueOf(age));
		CahAssert.assertEquals(rootNode.path("isStudent").asBoolean(), Boolean.valueOf(isStudent));
	}
	@Test
	public void test_FREEMARKER(String name, String age, String isStudent) {
		//Test FREEMARKER
		Map<String, String> dataModel = new HashMap<>();
		dataModel.put("name", "Jacky");
		dataModel.put("age", "14");
		dataModel.put("isStudent", "true");
		
		ComposeTemplate compoer = TemplateComposerProvider.FREEMARKER.getComposer();
    //test4-templates-freemarker.json could be store in both module folder and environment folder
		String content = compoer.compose(dataModel, dataClient.findTemplate("test4-templates-freemarker.json").get().toFile());
		logger.info(content);
		JsonNode rootNode = Utilities.jsonUtility.readTree(content);
		CahAssert.assertEquals(rootNode.path("name4").asText(), dataModel.get("name"));
		CahAssert.assertEquals(rootNode.path("age4").asInt(), Integer.valueOf(dataModel.get("age")));
		CahAssert.assertEquals(rootNode.path("isStudent4").asBoolean(), Boolean.valueOf(dataModel.get("isStudent")));
	}
	@Test
	public void test_ALL(String name, String age, String isStudent) {
		Map<String, String> dataModel = new HashMap<>();
		dataModel.put("name", "Mike");
		dataModel.put("age", "20");
		dataModel.put("isStudent", "true");
		
		String templateContent = "{\n"
				+ "	\"name\": \"${{name}}\",\n"
				+ "	\"age\": ${{age}},\n"
				+ "	\"isStudent\": ${{isStudent}}\n"
				+ "}";
		
		//Test CAH_COMPOSER
		logger.info("Test CAH_COMPOSER");
		ComposeTemplate compoer = TemplateComposerProvider.CAH_COMPOSER.getComposer();
		String content = compoer.compose(dataModel, templateContent);
		logger.info(content);
		JsonNode rootNode = Utilities.jsonUtility.readTree(content);
		CahAssert.assertEquals(rootNode.path("name").asText(), "Mike");
		CahAssert.assertEquals(rootNode.path("age").asInt(), 20);
		CahAssert.assertEquals(rootNode.path("isStudent").asBoolean(), true);
		
		//Test FREEMARKER
		logger.info("Test FREEMARKER");
		templateContent = "{\n"
				+ "\n"
				+ "	\"name\": \"${name}\",\n"
				+ "	\"age\": ${age},\n"
				+ "	\"isStudent\": ${isStudent}\n"
				+ "	\n"
				+ "}";
		
	    compoer = TemplateComposerProvider.FREEMARKER.getComposer();
		content = compoer.compose(dataModel, templateContent);
		logger.info(content);
		rootNode = Utilities.jsonUtility.readTree(content);
		CahAssert.assertEquals(rootNode.path("name").asText(), "Mike");
		CahAssert.assertEquals(rootNode.path("age").asInt(), 20);
		CahAssert.assertEquals(rootNode.path("isStudent").asBoolean(), true);
	}

8. SSH tunnel creation/deletion and remote execute SSH commands by JSCH, SSHJ, SSHD

User can choose the provider in global-config.properties. Multiple network tunnels can be created in one machine and network tunnel life cycle is managed by test.
And support both user password and private key.

#SSH Client: JSCH, SSHJ, SSHD 
SSH_Client=JSCH
    	HasSshTunnel sshTunnel = SshClientProvider.provide(host, user, password);
      //HasSshTunnel sshTunnel = SshClientProvider.provide(host, user, Paths.get("Path to private key"));
    	sshTunnel.connect();
    	logger.info("SSH tunnel is started....");
    	sshTunnel.setLocalPortForwarding(3000, "100.10.150.60", 80);
    	logger.info("SSH tunnel is stopped....");
    	sshTunnel.unsetLocalPortForwarding(3000);
    	sshTunnel.disconnect();
    	HasSshExec sshExec = SshClientProvider.provide(host, user, password);
      //HasSshTunnel sshTunnel = SshClientProvider.provide(host, user, Paths.get("Path to private key"));
    	sshExec.connect();
    	logger.info("Execute the first command");
    	String result = sshExec.executeCommand("cd ..; pwd; ls");
    	logger.info(result);
    	logger.info("Execute the second command");
    	result = sshExec.executeCommand("ping -c 3 bing.com");
    	logger.info(result);
    	sshExec.disconnect();

9. Native Logger

User can choose Logger Level(TRACE, DEBUG, INFO, WARN, ERROR) in global-config.properties

  • Global Config
    #Log Level: TRACE, DEBUG, INFO, WARN, ERROR
    Log_Level=INFO
    
  • Test code
      	Logger logger = Logger.getLogger();
      	logger.trace("");
      	logger.debug("");
      	logger.info("");
      	logger.warn("");
      	logger.error("");
    

10. CAH Assert

  • CahAssert is 100% compatible with testNG Assert. Actually CahAssert is the delegate of testNG Assert. CahAssert saves all assert detail information into AssertRecord, which will be displayed in reports.
  @Test
  public void testCahAssert() {
  	Object[] actual = {1.5, 1.6, 1.7, 1.8};
  	Object[] expected = {1.9, 1.6, 1.7, 1.8};
  	CahAssert.assertNotEquals(actual , expected, "Check if object array is equal");
  }
  • AssertRecord.Extra is information holder if need to save extra information into records, which will be displayed in reports. Extra information only be available and saved into next assert.
  @Test
  public void testCahAssert() {
  	Object[] actual = {1.5, 1.6, 1.7, 1.8};
  	Object[] expected = {1.9, 1.6, 1.7, 1.8};
    AssertRecord.Extra.setExtra("This is the first extra information for testCahAssert");
  	CahAssert.assertNotEquals(actual , expected, "The first extra information is available and saved into this assert");
    CahAssert.assertNotEquals(actual , expected, "The first extra information is not available and saved into this assert");
    AssertRecord.Extra.setExtra("This is the second extra information for testCahAssert");
    CahAssert.assertEquals(actual , expected, "The second extra information is available and saved into this assert");
  }

11. Test reporting

  • Annotation for report: @Description

    	@Test
      @Description(testIDs = "T-001", 
      title = "Knock Bing By Browser",
      submodule = "UI testing",
      description = "Knock bing by keywords ${{0}} and check search result with ${{1}}")
      public void testKnockBingByBrowser(String keywords, String result) {}
    
  • Recorder: record test flow and status during execution and show in report

    	 Recorder.log(String msg);
      Recorder.log(Recordable< ? > rec);
    
  • Predefined record types: CommonRecord, ScreenshotRecord, AssertRecord, ActionRecord, StepRecord

    • CommonRecord
      Take a note
    • ScreenshotRecord
      Take a screenshot
    • AssertRecord
      It is generated automatically during invokde CahAssert methods, and save the assertion detail then show in report
    • ActionRecord
      It is generated automatically if use ECO mode, and save the action detail then show in report
    • StepRecord
      It is generated automatically if use ECO mode, and save the step detail then show in report
  • Report type: CONSOLE, EXCEL, HTML_BOOTSTRAP, HTML_VUE_VUETIFY User can choose report types in global-config.properties. Report contains Test ID, Name, Description, Duration, Recording for each case, Errors, Exceptions, Logs and Screenshot for failed cases

      #Report type(support multiple reports): CONSOLE, EXCEL, HTML_BOOTSTRAP, HTML_VUE_VUETIFY
      Report_Type=CONSOLE, EXCEL, HTML_BOOTSTRAP, HTML_VUE_VUETIFY
    
    • Console report console-report

    • HTML_BOOTSTRAP report (ONE page) html_bootstrap-report html_bootstrap-report2 html_bootstrap-report3 html_bootstrap-report4

    • HTML_VUE_VUETIFY report (all in ONE page) html_vue_vuetify-report html_vue_vuetify-report2 html_vue_vuetify-report3 html_vue_vuetify-report4 html_vue_vuetify-report5 html_vue_vuetify-report9 html_vue_vuetify-report9 html_vue_vuetify-report6 html_vue_vuetify-report7 html_vue_vuetify-report8 html_vue_vuetify-report9

    • EXCEL report xls-report xls-report2

  • Performance monitoring reports: CONSOLE, EXCEL, HTML_BOOTSTRAP
    User can choose monitor providers in global-config.properties

      #Monitor Provider(support multiple provider): Http_Archiver_Monitor, Web_API_Perf_Monitor, Web_UI_Perf_Monitor
      Monitor_Provider=Web_API_Perf_Monitor, Web_UI_Perf_Monitor
      #Enable Performance Monitoring: TRUE, FALSE
      Enable_Performance_Monitor=TRUE
    
    • Http_Archiver_Monitor
      Recording all http activities for both Web UI test and Restful API test, finally http archive file is generated like 'CAH_AT_Performance.har'. Then view it by http archive viewer
      http-archive-viewer
    • Web_API_Perf_Monitor
      • Console Web API Performance Report console-report-web-api-perf
      • HTML Web API Performance Report html-report-web-api-perf
      • XLS Web API Performance Report xls-report-web-api-perf
    • Web_UI_Perf_Monitor
      • Console Web UI Performance Report console-report-web-ui-perf
      • HTML Web UI Performance Report html-report-web-ui-perf
      • XLS Web UI Performance Report xls-report-web-ui-perf
        xls-report-web-ui-perf2

12. ECO Design Pattern

  • Auto-detect automation accident and auto-retry at right place and right timing within a minimum scope, and no retry code in test
  • Roles: Executor, Controller, Operator (ECO)
  • Scopes: Test, Step, Action
  • Stop retry with two conditions
    • Reach max retry threshold
    • Face critical errors (java.lang.Error, java.lang.AssertionError, tech.testnx.cah.common.exceptions.StopTestError)
  • ECO Model
    ECO-Model
  • ECO Flow
    ECO-Flow
  • Example code
    • Executor
@Executor
public class AdvancedTests extends TestBase {
	
	private BingController bingController;
	private WebDriver driver;
	
    @BeforeMethod
    public void beforeMethod() {
        driver = getDriver();
        bingController = new BingController(driver);
    }
    
    @Test
    @Description(testIDs = "T-001", 
    title = "Bing Search", 
    submodule = "bing", 
    description = "Search Bing by keywords ${{0}}")
    public void testBingSearch(String keywords, String expectedTitle) {
        String url = "https://bing.com";
        bingController.searchAndCheck(url, keywords, expectedTitle);
    }
}
  • Controller
@Controller
public class BingController extends ControllerBase {
	
	private BingSearchPO bingSearchPO;
	private WebDriver driver;

    public BingController(WebDriver driver) {
        this.driver = driver;
        bingSearchPO = new BingSearchPO(driver);
    }
    
    protected BingController() {}
    
    @Step(description = "Search Bing and check result", retry = 2, intervalWaitInSecond = 3)
    public void searchAndCheck(String url, String keywords, String expectedTitle) {
        driver.get(url);
        bingSearchPO.search(keywords);
        String result = bingSearchPO.getResultTitle();
        logger.debug("Search result title: " + result);
        Assert.assertTrue(result.equalsIgnoreCase(expectedTitle));
    }
}
  • Operator
@Operator
public class BingSearchPO extends PageBase {

    @FindBy(name = "q")
    private WebElement searchbox;
    
    @FindBy(id = "search_icon")
    private WebElement searchButton;
	
    public BingSearchPO(WebDriver driver) {
        super(driver);
    }
    
    protected BingSearchPO() {}
	
	@Override
	public void waitForReady() {}
	
    /**
     * Action: Search Bing by keywords
     */
    @Action(description = "Search Bing by Keywords: ${{0}}", retry = 2, intervalWaitInSecond = 3)
    public void search(String keywords) {
    	waitForReadyAndCapturePagePerf();
        searchbox.clear();
        searchbox.sendKeys(keywords);
        Utilities.timeUtility.sleep(2);
        searchButton.click();
    }
    
    /**
     * Action: Get result title
     */
    @Action(description = "Get result title", retry = 2, intervalWaitInSecond = 3)
    public String getResultTitle() {
    	waitForReadyAndCapturePagePerf();
    	return driver.getTitle();
    }
}