The User Interface defines how the application works. Automated testing from the user interface is therefore a must-do. And since Front End Engineers are in charge of developing the user interface the best investment you can do for Quality Assurance ( QA ) is to have Front End Engineers responsible for testing the application.
No need to mention how important is to have automated tests as part of any SDLC. Unfortunately for many projects it is prohibited to do any kind of Test Driven Development (TDD). Whatever the reason is (and yes, there are reasons that are sometimes beyond the IT team control) still Tests are necessary.
The tests you cannot forget about are those resulting from existing bugs (Test Driven Bug Fix or TDBF). When you correct something you should write a test that asserts that part of the application works as expected. The reason is that you want to be sure the very same problem does not hit you back. It is embarrassing to hear from Business side "This problem is happening again", period.
So you might not be able to afford TDD but at least you should be able to provide Development with Automated Tests (Let us call it DAT as these days we are supposed to remember abbreviations for almost anything :-)
While U-TDD (Unit TDD) is in reality invisible for the stake holders A-TDD (Acceptance TDD) is not.
User Stories have proven to be an effective mechanism to allow both, business and developers talk the same language. Plain English constructs "Given, When, Then" are a good starting point to build automated acceptance tests. There are multiple testing frameworks supporting code assertions for user stories. Selenium is one that I think is leading the race.
Your Test team (also called QA) can use any language regardless of the language being used for your real website. The reality is that small and medium size companies commonly cannot afford paying for dedicated people for this matter (Even though this is the ideal way to go either for Agile or Waterfall approaches). But as an experienced software architect you know that separation of concerns is the most important principle of successful enterprise software development so even if you decide to use the same language and keep it maintained by the same team of programmers you should:
1. Convince stake holders they must agree on User Stories.
2. Put the acceptance tests that assert the user stories in an acceptance tests project (Do not use your application project to host acceptance tests).
3. Externalize any configurations as URLs and credentials.
4. Schedule (at least daily) automated acceptance tests. Continuum Integration is a must-have. I have built it myself in the past but do not waste your time there are really good tools in the market for this.
The development team must be focused on what the customer really wants. Your customer (stake holders) must be in control to evaluate if the software you are building meets the requirements. There is then,
as I have written before, a need for business people to write effective user stories.
Here is how the User Story 1 should be written by our stake holders. Note that we could divide this User Story in more than just one but for the purpose of showing the use of differenct methods in the final Java class I prefer to include all related scenarios:
User Story 1:
------------
1.1
Given a user is not logged in
When any page is hit
Then the user should be asked to authenticate
1.2
Given a user with admin role is logged in
When an admin page is hit
Then the user should see the page
1.3
Given a user with just regular user role is logged in
When an admin page is hit
Then the user should see an error page
In the Java world I prefer Maven as building tool. Below you can find what a POM file will look like to use Selenium 2 for Acceptance tests.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nestorurquiza</groupId>
<artifactId>acceptance-tests</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>acceptance-tests</name>
<packaging>jar</packaging>
<description>Acceptance tests for my web application</description>
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>selenium-java</groupId>
<artifactId>selenium-java</artifactId>
<version>2.0a6</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<defaultGoal>install</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.0-beta-7</version>
<configuration>
<tagBase>
http://my.svn.repo/acceptance-tests/tags/
</tagBase>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.7</version>
<configuration>
<ajdtVersion>2.0</ajdtVersion>
<wtpversion>2.0</wtpversion>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>central</id>
<name>libs-releases-local</name>
<url>
http://my.artifacts.repo/libs-releases-local
</url>
<uniqueVersion>false</uniqueVersion>
</repository>
</distributionManagement>
<scm>
<connection>scm:svn:http://my.svn.repo/acceptance-tests/tags/trunk/</connection>
</scm>
</project>
Below is an example of a properties file to hold the target application URL, the WebDriver to use (HTMLUnit driver in this case) and user names and passwords to test:
#acceptanceEnvironment.propeties
baseUrl=http://localhost:8080
webDriverClass=org.openqa.selenium.htmlunit.HtmlUnitDriver
adminUserName=admin@nestorurquiza.com
adminUserPassword=test
regularUserName=user@nestorurquiza.com
regularUserPassword=test
Here is a class you can use to get the properties from the file above in you acceptance tests:
package com.nestorurquiza.acceptance;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
/**
* A VM argument providing the properties path parameter is needed.
* for example: -DacceptanceEnvironment.properties=/Users/nestor/projects/config/acceptanceEnvironment.properties
*
* Here is an example of such a property file
* * #acceptanceEnvironment.propeties
* baseUrl=http://localhost:8080
* webDriverClass=org.openqa.selenium.htmlunit.HtmlUnitDriver
* adminUserName=admin@nestorurquiza.com
* adminUserPassword=test
* regularUserName=user@nestorurquiza.com
* regularUserPassword=test
*
*
*/
public class EnvironmentProperties {
private static final String ACCEPTANCE_ENVIRONMENT_PROPERTIES = "acceptanceEnvironment.properties";
private static final EnvironmentProperties INSTANCE = new EnvironmentProperties();
private static Properties properties = null;
private EnvironmentProperties() {
}
public static Properties getInstance() throws FileNotFoundException {
if(properties == null) {
properties = new Properties();
//Use -DacceptanceEnvironment.properties=/Users/nestor/projects/config/acceptanceEnvironment.properties
String acceptanceEnvironmentPropertiesPath = System.getProperty(ACCEPTANCE_ENVIRONMENT_PROPERTIES);
if(acceptanceEnvironmentPropertiesPath != null && acceptanceEnvironmentPropertiesPath.trim().length() > 0){
InputStream in = new FileInputStream(acceptanceEnvironmentPropertiesPath);
if(in != null) {
try {
properties.load(in);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return properties;
}
public static WebDriver getWebDriver() throws FileNotFoundException {
//The simplest not environment specific Driver
WebDriver webDriver = new HtmlUnitDriver();
Class c;
try {
c = Class.forName(EnvironmentProperties.getInstance().getProperty("webDriverClass"));
webDriver = (WebDriver) c.newInstance();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return webDriver;
}
public static String getBaseUrl() throws FileNotFoundException {
return EnvironmentProperties.getInstance().getProperty("baseUrl");
}
public static String getAdminUserName() throws FileNotFoundException {
return EnvironmentProperties.getInstance().getProperty("adminUserName");
}
public static String getAdminUserPassword() throws FileNotFoundException {
return EnvironmentProperties.getInstance().getProperty("adminUserPassword");
}
public static String getRegularUserName() throws FileNotFoundException {
return EnvironmentProperties.getInstance().getProperty("regularUserName");
}
public static String getRegularUserPassword() throws FileNotFoundException {
return EnvironmentProperties.getInstance().getProperty("regularUserPassword");
}
}
Finally here is a sample acceptance test that makes several assertions, all of them included in our User Story 1. Note that the test below asserts a site protected with Spring Security:
package com.nestorurquiza.acceptance;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import java.io.FileNotFoundException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
/**
* Verifying behavior of UserStory1.
*/
public class UserStory1Test {
private WebDriver driver = null;
@Before
public void aTearUpMethodForEachTest() throws FileNotFoundException {
driver = EnvironmentProperties.getWebDriver();
}
@After
public void aTearDownMethodForEachTest() {
driver.close();
}
@Test
public void givenNotLoggedInWhenAnyPageIsHitThenShouldBeAskedToAuthenticate() throws FileNotFoundException {
String url = EnvironmentProperties.getBaseUrl() + "/client/list";
driver.get(url);
assertThat(driver.findElement(By.name("loginForm")).getAttribute("action"), is("j_spring_security_check"));
}
@Test
public void givenLoggedInAndAdminUserRoleWhenAnAdminPageIsHitThenShouldSeeThePage() throws FileNotFoundException {
String url = EnvironmentProperties.getBaseUrl() + "/";
driver.get(url);
WebElement loginForm = driver.findElement(By.name("loginForm"));
WebElement userName = loginForm.findElement(By.name("j_username"));
userName.sendKeys(EnvironmentProperties.getAdminUserName());
WebElement password = loginForm.findElement(By.name("j_password"));
password.sendKeys(EnvironmentProperties.getAdminUserPassword());
loginForm.submit();
url = EnvironmentProperties.getBaseUrl() + "/client/list";
driver.get(url);
//Not supported by HtmlUnit
//assertThat(driver.findElement(By.cssSelector(".form-navigation")), is(notNullValue()));
assertThat(driver.findElement(By.className("form-navigation")), is(notNullValue()));
}
@Test
public void givenLoggedInAndRegularUserRoleWhenAnAdminPageIsHitThenShouldSeeAnErrorPage() throws FileNotFoundException {
String url = EnvironmentProperties.getBaseUrl() + "/";
driver.get(url);
WebElement loginForm = driver.findElement(By.name("loginForm"));
WebElement userName = loginForm.findElement(By.name("j_username"));
userName.sendKeys(EnvironmentProperties.getRegularUserName());
WebElement password = loginForm.findElement(By.name("j_password"));
password.sendKeys(EnvironmentProperties.getRegularUserPassword());
loginForm.submit();
url = EnvironmentProperties.getBaseUrl() + "/client/list";
driver.get(url);
//Not supported by HtmlUnit
//assertThat(driver.findElement(By.cssSelector(".form-navigation")), is(nullValue()));
assertThat(driver.findElement(By.className("errors")), is(notNullValue()));
}
}
Below is how to run our Acceptance Tests from Maven:
mvn test -DargLine="-DacceptanceEnvironment.properties=/Users/nestor/projects/config/acceptanceEnvironment.properties"
To run the Acceptance Tests from an IDE you will need to configure a VM argument like
-DacceptanceEnvironment.properties=/Users/nestor/projects/config/acceptanceEnvironment.properties
You can of course change the driver so you use different browsers. As the available drivers[1] support javascript from a front end perspective you can come up with very complete test assertions that later can be scheduled so you keep checking the website functionality in a regular basis.
I have to say though that to test Front End development you better wait for the preference of your UI Engineer. Java is most likely not his preferred language.
While I here provide guidance to ATDD from a Java perspective I still think that given the fact that ATDD is closer to front end development or User experience (UX) it should be better to study other possibilities in the case you have the luxury of having a UX engineer on board.
Bibliography
1.
http://seleniumhq.org/docs/09_webdriver.html
2.
http://thinkinginsoftware.blogspot.com/2010/08/writing-agile-specifications.html