Sunday 19 October 2008

Noisy Scenario Methods

This is the second entry in a series of posts that provide practical advice on how to avoid or refactor away from common functional testing smells.

Noisy scenario methods can generally be identified by the presence of long methods, which fail to highlight the important interactions with the application clearly. The code driving the application will typically be at a low level of abstraction adding to the noise. For web applications this will involve requesting resources, posting parameters and ensuring the expected response is returned. For example:


public class SignInAcceptanceScenarios {

@Test
public void shouldPresentKnownUserWithTheWelcomePage() {

User user = new UserBuilder().
withUsername("ryangreenhall").
withPassword("password").
build();

UserRespository respository = new UserRespository();
respository.create(user);

WebDriver driver = new HtmlUnitDriver();
driver.get("http://www.example.com");

driver.findElement(By.id("username")).sendKeys("ryangreenhall");
driver.findElement(By.id("password")).sendKeys("password");
driver.findElement(By.id("login")).submit();

Assert.assertEquals("Welcome", driver.getTitle());
}
}

When reading scenarios I like to see clear descriptions of the steps that are taken when interacting with the application. The main problem with noisy scenario methods is that the reader has to filter through the noise to figure out the following: what is the starting state of the system? What interactions occur? An additional problem is that the reader is required to think at a low level of abstraction. Rather than thinking about signing in to the application we are forced to think in terms of posting parameters representing the username and password to a sign-in resource.

Discovering the Scenario Steps

Reading the example scenario we can see that ensuring a known user is presented with the welcome page requires the following steps:

  1. Create a user
  2. Navigate to the applications home page
  3. Fill in the sign in form with known credentials
  4. Submit the signin form
  5. Ensure that the user is taken to the welcome page

Some would argue that the example scenario could be improved with the introduction of comments prior to each step. However, I favour scenarios that are composed of small methods, whose names clearly describe: the starting state of the application, the interactions with the application and the expected outcomes. This allows the scenario to be expressed in terms of the domain without distracting the reader with the implementation details.

The Given-When-Then scenario format popularised by Behaviour Driven Development is a good starting point for structuring scenarios.

Making Your Code Read Like a Scenario

The following example shows how the original scenario has been improved using a series of Extract Method refactorings in order to communicate the important steps in the scenario, namely: create a new user, sign in with known credentials and ensure that the user is presented with the welcome page.


public class SignInAcceptanceScenarios {

private WebDriver driver;

@Before
public void setup() {
driver = new HtmlUnitDriver();
}

@Test
public void shouldPresentKnownUserWithTheWelcomePage() {

Credentials credentials = new Credentials("ryangreenhall", "password");

givenAnExistingUserWith(credentials);
whenUserLogsInWith(credentials);
thenTheUserIsPresentedWithTheWelcomePage();
}

private void givenAnExistingUserWith(Credentials credentials) {
User user = new UserBuilder().
withCredentials(credentials).
build();

UserRespository respository = new UserRespository();
respository.create(user);
}

private void whenUserLogsInWith(Credentials credentials) {
browseToHomePage();
enterUsernameAndPassword(credentials);
clickSignInButton();
}

private WebDriver browseToHomePage() {
driver.get("http://www.example.com/sign-in");
return driver;
}

private void enterUsernameAndPassword(Credentials credentials) {
driver.findElement(By.id("username")).sendKeys(credentials.getUserName());
driver.findElement(By.id("password")).sendKeys(credentials.getPassword());
}

private void pressSignInButton() {
driver.findElement(By.id("login")).submit();
}

private void thenTheUserIsPresentedWithTheWelcomePage() {
Assert.assertEquals("Welcome", driver.getTitle());
}
}

Structuring scenario methods in this style allows the reader to quickly understand the behaviour expected of the application in the context of a given scenario. Furthermore given that they read like written scenarios in terms of the problem domain they can be used to assist in conversations with QAs and Business Analysts, in addition to serving as useful documentation for future maintainers.

No comments: