Create a java automated test

The purpose of this document is to explain how to create a new test class in an existing test project. It contains technical explanation about how automated tests work.

Project in the forge : https://scm.gforge.inria.fr/svn/gazelle/Maven/gazelle-gui-testing/Automated-Test-testNG/branches/idr_testing

The project has three important parts :

  • Test classes
  • PageObject classes
  • XPath catalog

Tests and PageObject classes

The project uses the design pattern page object. Test classes do not interact with webpage directly. All page interactions are factored in PageObject classes. This way if an action is required by several tests, it is written only once in one PageOblect class. PageObject classes are "interfaces" between webpages and test classes.

Test classes overview

All test classes extends BaseTest. Two methods must be defined : run() and testScenario(Object dataSet). The skeleton of a test class looks like this :

public class TM123 extends BaseTest {	
	@Override
	@Test
	public void run() {
		// Dataset initialization.
		// Usually the 1st one is a LoginProfile
		Object[][] dataSets = new Object[][] {
			{ new LoginProfile("login", "password",
			           Languages.ENGLISH, "Some TestingSession"),
			  "Some Text" },
			{ /* Other dataSet */ }
		};
	withDataSets(dataSets);
	super.run();
	}
	
	@Override
	public void testScenario(Object[] ds) {
		LoginProfile loginProfile = (LoginProfile)ds[0];
		String someText = (String)ds[1];

		// Most of the tests start like this :
		// Open the main page, log-in and set the default testing session
		MainPage mp = new MainPage(getDriver(), loginProfile);
		mp.go();
		// Page command.
		// Notice that there should be only PageObject requests
		MiscPageObject mpo = mp.clickOnSomeLink();
		mpo.fillInSomeText(someText);
		// Assertions. Again, they are about PageObject method,
		// not about the page directly
mp.logout(); }

 run() is the entry point for TestNG. It must always be annoted @Test. The class must not be annoted @Test.

Why are there 2 methods for one single test ?

There was initially only one method, run(). After some work it appeared that the test sometime failed for reasons that could not be considered as a "tested functionnality malfunction" (e.g. internet connection problem, etc.). The solution to this was to run the test method several times until it finished without encountering "external" failures. There was another constraints : having the entry point of a test in each class. It is important to have the entry point in the test class, without it we would have to write an other file whose function would be to reference all test class. We would then have to maintain this file, etc. The design pattern "template" was used.

BaseTest does one big thing : its run() method does some initialization (required by every test) and calls testScenario() until it ends successfully or with a decisive assertion. The method testScenario is the one which is implemented in every test class. Also, every test class' run() has to call super.run(). The overall functionning works like this :

  • TestNG find a class with a method annoted @Test. In our case this method is always (and only) run()
  • The run() of a concrete test class is called
  • Some data sets initializations are made
  • super.run() is called, the same for every test class : the one of BaseTest
  • BaseTest.run() does some 'universal' initialization
  • BaseTest.run() calls testScenario() until it returns properly and the testScenario() of the concrete test class is called

XPath catalog and PageObject functionning

All webpage query are made using xPath and not component Ids. This is because, the way GazelleTM is developped, Ids are redefined randomly everytime jboss restarts (and are thus uselss for identification). XPaths can be viewed as a description of a web component. If the component change in a non-essential way, the xPath still selects the same component, which is good; but the component can change too much, or a new component can be added to the page, making the xPath ambigous between the old and the new component (the xPath selects them both). To (partly) solve this problem, all xPath are stored in a single file and most of them have a part that is dynamically loaded.

The file xpath.properties contains all xPaths that are used in PageObject classes. All used xPath should be placed in this file. If possible, all "essential" text of a component used in an xPath should not be written directly in this file and should be loaded dynamically in the PageObject (e.g. the name of a button). Before an xPath can be used by a PageObject class, it should be loaded in its xPath hasmap. BasePage, whose every PageObject inherits, does one important things : it encapsulates all xPath queries. BasePage has a hasmap <String, String> of xPath. The key is the name of the xPath, close to the key of the xPath in the xpath.properties. The value is the xPath found xpath.properties filled with data. All xPath should be loaded and stored in the hasmap at the construction of the PageObject. Then, all xPath query should be done by calling the getWebElement() method of BasePage. Here is a skeleton shared by all PageObject class :

public class MiscPageObject extends BasePage{
	public PreConnectathonResults(WebDriver driver, LoginProfile lp) {
		super(driver, Loader.instance().config("base.url")
			Pages.CAT_PRECAT_VALIDATION_RESULT.getLink()
			.replace(".xhtml", ".seam"), lp);
	}
	
	@Override
	protected void loadXPaths() {
		// BasePage.loadXPaths() loads some xpath used by everything
		super.loadXPaths();

		// xPaths is the hashMap of all used xPath in this class
		xPaths.put("TextBox.Status",
			MessageFormat.format(
				Loader.instance().xpath("MiscPageObject.TextBox.Status.XPath"),
				Loader.instance().crowdin("gazelle.tf.table.Status")));
		xPaths.put("Span.Name",
				Loader.instance().xpath("MiscPageObject.Span.Name.XPath"));

	}

	/**
	 * Enters a status in the textbox. Presses enter
	 */
	public void fillInTextbox(String text) {
		// Notice how we get the WebElement only through getWebElement
		getWebElement("TextBox.Status").clear();
		sleep(1);
		getWebElement("TextBox.Status").sendKeys(text);
		getWebElement("TextBox.Status").sendKeys(Keys.RETURN);
	}
	
	/**
	 * Gets the content of the textbox
	 */
	public String getTestStatus(String testNumber, String keyword) {
		return getWebElement("Span.Name", keyword, testNumber).getText();
	}
}

 All WebElement query is fetched only getWebElement(). This allows one thing : if the xPaths is unable to select an element, we can track immediatly the error and display the name of the xPath that failed. It is important to have the name instead of the value of the xPath, because the value of the xPath can be long and tough to read.

 Step-by-step tutorial

We will take a simple example : the test we want to write checks a login feature. We login and we check that the displayed name is properly our.

The test to automate

Our two pages have this html code :

Login page :

<html>
 <h1>Login</h1>
 <table>
  <tr>
   <td>Username</td>
   <td><input type="textbox" /></td>
  </tr>
  <tr>
   <td>Password</td>
   <td><input type="textbox" /></td>
  </tr>
  <tr>
   <td><input type="button" value="Login" /></td>
  </tr>
 </table>
 
 <!-- some other content ... -->

</html>

Profile page :

<html>
 <h1>User Page</h1>
 <table>
  <tr>
   <td>Username</td>
   <td><span>Teemo</span></td>
  </tr>
  <tr>
   <td>Address</td>
   <td><span>blablabla</span></td>
  </tr>
  <tr>
   <td>City</td>
   <td><span>bla</span></td>
  </tr>
 </table>
 
 <!-- some other content ... -->

</html>

Let's write down exactly what our test has to do :

  • Fill in the "Username" textbox
  • Fill in the "Password" textbox
  • Click the "Login" button
  • Check that the displayed username is "Teemo"

The first step is to list every component that will be used. Here :

  • The Username textbox
  • The Password textbox
  • The login button
  • AND the username span

XPath catalog

The second step is to create the xPaths. This part is kind of tricky. The goal is to have an xPath flexible enough to handle some changes but rigid enough to select only the one component we want. Here is one solution :

For the Username textbox : //table//tr[td[text()='Username']]/td/input[@type='textbox']

This xPath means : "The first textbox you find (input[@type='textbox']) in the row (tr) that contains a cell (td) that has the text "Username ([text()='Username']).

For the Password textbox we can use the same, given we change "Username" into "Password" : //table//tr[td[text()='Password']]/td/input[@type='textbox']

Notice the "//" before table and tr. It means that the table can be anywhere in the page and that the line (tr) can be anyone. This is a change resistance.

For the login button, we can take this simple one : //input[@type='button' and @value='Login']

This one selects the button whose text is "Login". It is very unlikely that another button labelled "Login" will be put in the page in the future, so we can keep this wide xPath : the button can now move anywhere in the page, we will still get it.

For the Name span we can use pretty much the same as for the previous texboxes : //table//tr[td[text()='Name']]/td[2]/span

It means "The span of the second cell (/td[2]/span) of the row that contains a cell whose text is "Name" (//tr[td[text()='Name']])

We now have our xPath, but we have important text in them ("Username", "Login"). This is something we can extract and load dynamically, this way if the text is changed by Gazelle's developper, it will be instantly changed in our xPaths (change resistance).

So, let's replace every text by a {x} so we can fill them in later in our PageObject classes :

  • Username textbox : //table//tr[td[text()=''{0}'']]/td/input[@type=''textbox'']
  • Password textbox : //table//tr[td[text()=''{0}'']]/td/input[@type=''textbox'']
  • Login button : //input[@type=''button'' and @value=''{0}'']
  • Name span : //table//tr[td[text()=''{0}'']]/td[2]/span

Note how all quotes became quoted. This is because every curly bracket is used by MessageFormat. To display a bracket we have to escape it. We, here, don't need to display any bracket .. but the escape character is the quote ' so we have to escape every quote.

We can also notice that the Username and Password textbox are exactly the same once we have extracted their particular text. We can, in the xpath.properties, use only one xpath for the two (here it would be named simply "textbox" or something) but we can keep the two like this. It create a duplication, but if we chose to merge the two into one, maybe one day only one element will change. We will then need to split the xpath into two (for example if "Username" is displayed in bold and not "Login"), which is as anoying as keeping a code duplication. There is no one answer here, sometimes it is good to keep two separate xpath, sometimes it is good to have only one. In this case we will merge the two.

What we have to write in our xpath.properties is thus :

LoginPage.TextBox.XPath=//table//tr[td[text()=''{0}'']]/td/input[@type=''textbox'']
LoginPage.Button.Login.XPath=//input[@type=''button'' and @value=''{0}'']
UserPage.Span.Username.XPath=//table//tr[td[text()=''{0}'']]/td[2]/span

 Note how we name our properties : [PageConcerned].[TypeOfComponent].[DetailAboutTheComponent].XPath

At this point we are done with the xpath catalog. Let's work on the PageObject classes.

PageObject classes

We have two different page. We will thus make two PageObject classes. Note that it is arbitraty : it can be adequate to make one class for just a part of a page. The goal is to have a functionnal separation between files.

Let's create our file Login.java. It will contain the skeleton shown above :

public class MiscPageObject extends BasePage{
	public LoginPage(WebDriver driver, LoginProfile lp) {
		super(driver, Loader.instance().config("base.url")
			Pages.LOGIN_FOR_OUR_EXAMPLE.getLink()
			.replace(".xhtml", ".seam"), lp);
	}

	@Override
	protected void loadXPaths() {
		// BasePage.loadXPaths() loads some xpath used by everything
		super.loadXPaths();
	}
}

The first thing to do is to set the default url for the page. We usually want to use a value found in the Page.class enum of the Gazelle project.

Then, we have to load the xPaths we defined above :

@Override
protected void loadXPaths() {
	super.loadXPaths();

	xPaths.put("TextBox.Username",
		MessageFormat.format(
			Loader.instance().xpath("LoginPage.TextBox.XPath"),
			Loader.instance().crowdin("properties.for.username"));
	xPaths.put("TextBox.Password",
		MessageFormat.format(
			Loader.instance().xpath("LoginPage.TextBox.XPath")
			Loader.instance().crowdin("properties.for.password"));
	xPaths.put("Button.Login",
		MessageFormat.format(
			Loader.instance().xpath("LoginPage.Button.Login.XPath")
			Loader.instance().crowdin("properties.for.login"));
}

Note how we fill the xPath with data found with the Gazelle crowdin properties.

Once this is done, we have to create the methods that will be used in our test. We will need :

  • A method to fill in Username
  • A method to fill in Password
  • A method to click on Login

There is no a priori good level of granularity. We can either have on single method for the three action or one for each. In this case one big method is fine : it is not currently likely that we will need to fill in the Username and not the password. But this could be the case, for other tests. Anyway, the goal is to anticipate which solution will last longer. In our case, we groupe the three tasks into one.

It gives us :

public UserPage login(String username, String password) {
	getWebElement("TextBox.Login").clear();
	getWebElement("TextBox.Password").clear();
	getWebElement("TextBox.Login").sendKeys(username);
	getWebElement("TextBox.Password").sendKeys(password);
	getWebElement("Button.Login").click();

	return new UserPage(getDriver(), getLoginProfile());
}

Note that we return a UserPage. This is because we know that we are supposed to be redirected to a user page : it allows method chaining in the test classes.

We won't need anything more with that page for now (fot this test). We are done with this page, but if we need, for another test, new things in this page, we will add the new methods here.

The class UserPage.class, meanwhile, will look like this :

public class UserPage extends BasePage{
	public PreConnectathonResults(WebDriver driver, LoginProfile lp) {
		super(driver, Loader.instance().config("base.url")
			Pages.USERPAGE_FOR_OUR_EXAMPLE.getLink()
			.replace(".xhtml", ".seam"), lp);
	}
	
	@Override
	protected void loadXPaths() {
		super.loadXPaths();

		xPaths.put("Span.Username",
			MessageFormat.format(
				Loader.instance().xpath("UserPage.Span.Username.XPath"),
				Loader.instance().crowdin("properties.for.username")));
	}

	public String getDisplayedName() {
		return getWebElement("Span.Username").getText();
	}
}

We are now done with PageObject, our pages should be able to initialize themselves and to query webpages.

Test classes

The shortest part remains : writing the tests. The result is the skeleton shown above plus methods call corresponding to the steps we identified above :

public class TMEXAMPLE extends BaseTest {	
	@Override
	@Test
	public void run() {
		Object[][] dataSets = new Object[][] {
			{ "Teemo", "ThePassword" }
		};
	withDataSets(dataSets);
	super.run();
	}
	
	@Override
	public void testScenario(Object[] ds) {
		String username = (String)ds[0];
		String password = (String)ds[1];

		UserPage up = new LoginPage(getDriver())
		  .login(username, password);
		
		Assert.assertequals(up.getDisplayedName(), username,
			"The displayed name should be : " + username);
	}
}

Note how we use the dataset to center the data at the same place. At the begining of testScenario() we extract the data from the array, for more readability, then we call the righ methods. At the end we perform an assert.

Our test is now ready. For debugging, it can be run with Alt+Shit+X then N (or in Run > Run as > TestNG test) when you're using Eclipse. If you're using Intellij IDEA you can run the current test by pressing Shift + F10 (or by clicking Run > Run 'CLASS_NAME'). Just commit it and it will automatically be run at the next test session. For automatic result logging, see this section.