Tuesday 21 October 2008

Exposed Scenario Implementation

In my previous post I refactored a noisy scenario method so that it communicated the required scenario steps more clearly. Although this was a huge improvement in terms of readability the SignInAcceptanceScenarios class still has a significant weakness; maintainability due to the exposure of a number of implementation details.

Here is the current state of the sign-in scenario:


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());
}
}

Violating the Single Responsibility Principle

Following the Single Responsibility Principle we know that a class should have only one reason to change. The SignInAcceptanceScenario is responsible for ensuring that the identified sign-in scenarios execute correctly. Let's consider how many reasons it has to change:

  1. The sign in resource changes: e.g. /log-in;
  2. We want to replace Web Driver with another web testing framework;
  3. The parameter names for username and password change;
  4. The title of the welcome page changes;
  5. The behaviour of the application changes;

We have identified that our scenario class has five reasons to change! The only valid reason for this class to change is when the behaviour of the sign in process changes. For example, rather than presenting the user with the welcome page they are taken to their profile page, a common feature for most social networking sites these days.

Clearly with so many reasons to change, acceptance scenarios written in this style have the potential to require many changes throughout the lifetime of the application.

Abstraction and Encapsulation to the Rescue

Wouldn't it be great if we could encapsulate our scenarios from implementation details such as the location of the sign-in resource and the parameter names used to communicate the users credentials? This would allow the implementation of the application to change without modifying our scenarios.

Currently when implementing the sign in scenario we are thinking in terms of the abstractions provided by the Web, for example, browse to the home page and submit parameters to a sign-in resource. Wouldn't it be great if we could raise the level of abstraction and just sign in to the application using credentials, asking the resulting page if it is the welcome page?

Let's briefly consider one possible approach.

Fluent Navigation

Rather than exposing the implementation details of how we are interacting with application we need a suitable abstraction. One such abstraction involves representing each page in the application as a Page Object. The Page Object Model, introduced to me by my colleague Dan Bodart and also recommended by the WebDriver team, nicely encapsulates the internal representation of a page and provides methods for appropriate interactions. Another nice feature of the Page Object Model is that we can make use of a fluent interface to allow seamless navigation through any number of pages.

The goal here is to demonstrate the value of introducing a suitable abstraction for interacting with the application. I will therefore leave detailed descriptions of the implementation in the interest of brevity.

SignInPage

Given that the scenario in the example involves signing in to the application we will need a SignInPage that allows credentials to be entered and submitted.

For example:


import com.example.domain.Credentials;

public class SignInPage implements Page {

public SignInPage() {
}

public SignInPage with(Credentials credentials) {
// enter the username and password
return this;
}

public Page submit() {
// submit the username and password to the sign-in resource
// and return a Page representation

// of the response.
}

public String getTitle() {
...
}
}

Application Facade

Now that we have a SignInPage how can our SignInAcceptanceScenarios class get hold of one? The SignInPage will be dispensed by a class called MyApp, which acts a facade for the application providing methods to access various pages in the application. This approach nicely decouples the scenario from the URLs used to address pages in the application.

For example:


import com.example.web.pages.HomePage;
import com.example.web.pages.Page;
import com.example.web.pages.SignInPage;

public class MyApp {

public static SignInPage signIn() {
// GET the sign in page and return a sign in page object
}
}

Matching Pages

We now have the ability to navigate to the sign-in page, enter the users credentials and submit, receiving a resultant page. The only remaining functionality required by the sign-in scenario is to ensure that the page returned as a result of submitting user credentials is the welcome page. How can we encapsulate the sign-in scenario from knowing the internals of the welcome page? This sounds like a job for a Hamcrest matcher. We will create a WelcomePageMatcher that knows if a given page is the welcome page by inspecting the title of the page.

For example:


import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import com.example.web.pages.Page;

public class WelcomePageMatcher extends BaseMatcher {

public boolean matches(Object actualPage) {
return "Welcome".equals(((Page)actualPage).getTitle());
}

public void describeTo(Description description) {
}

public static WelcomePageMatcher isWelcomePage() {
return new WelcomePageMatcher();
}
}

Bringing Everything Together

The application facade, SignInPage and WelcomePageMatcher can be combined to provide a very concise specification of the behaviour required when users sign-in to the example application.


import com.example.domain.builders.UserBuilder;
import com.example.domain.Credentials;
import com.example.domain.User;
import com.example.persistence.UserRespository;
import static com.example.web.MyApp.signIn;
import com.example.web.pages.Page;
import static com.example.web.pages.matchers.WelcomePageMatcher.isWelcomePage;
import org.junit.Test;

public class SignInAcceptanceScenarios {

@Test
public void shouldPresentKnownUserWithTheWelcomePage() {
Page pageAfterLogIn = null;
Credentials credentials =
new Credentials("ryangreenhall", "password");

givenAnExistingUserWith(credentials);
whenUserLogsInWith(credentials, pageAfterLogIn);
thenUserIsPresentedWithTheWelcomePage(pageAfterLogIn);
}

// Alternatively we could have:
@Test
public void shouldPresentKnownUserWithTheWelcomePage() {
Credentials credentials =
new Credentials("ryangreenhall", "password");

// Given
givenAnExistingUserWith(credentials);

// When
Page pageAfterLogin = signIn().with(credentials).submit();

// Then
ensureThat(pageAfterLogin, isWelcomePage());
}

// Alternatively we can combine the when and then steps in a single line:
@Test
public void shouldPresentKnownUserWithTheWelcomePage() {
Credentials credentials =
new Credentials("ryangreenhall", "password");

givenAnExistingUserWith(credentials);

ensureThat(signIn().with(credentials).submit(),
respondsWithWelcomePage());
}


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

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

private void whenUserLogsInWith(Credentials credentials, Page pageAfterLogIn) {
pageAfterLogIn = signIn().with(credentials).submit();
}

private void thenUserIsPresentedWithTheWelcomePage(Page pageAfterLogin) {
ensureThat(page, isWelcomePage());
}
}

Summary

We have seen that the introduction of a simple internal DSL to interact with the application has greatly improved the readability of this scenario. Furthermore the SignInAcceptanceScenarios class is now completely decoupled from the implementation of the application making it less susceptible to change.

We can now change the following concerns without modifying the SignInAcceptanceScenario:

  1. Location of the sign in resource (encapsulated in the SignInPage class);
  2. Web testing framework (encapsulated in both the Application Facade and Page classes);
  3. The parameter names for username and password (encapsulated in the SignInPage);
  4. The title of the welcome page changes (encapsulated in the WelcomePageMatcher);

The SignInAcceptanceScenarios class now has only one responsibility; defining the behaviour of the application and thus should only require modification when the behaviour of the application changes.

No comments: