Writing acceptance tests

Unit tests can only cover a subset of the different interactions between the components of our application. To go a little further, we will need to set up acceptance tests, tests that will actually boot up the complete application and allow us to interact with its interface.

The Gradle configuration

The first thing we will want to do when we add integration tests to a project is to put them in a different location to that of the unit tests.

The reason for this is, essentially, that acceptance tests are slower than unit tests. They can be part of a different integration job, such as a nightly build, and we want developers to be able to launch the different kinds of tests easily from their IDE. To do this with Gradle, we will have to add a new configuration called integrationTest. For Gradle, a configuration is a group of artifacts and their dependencies. We already have several configurations in our project: compile, testCompile, and so on.

You can have a look at the configurations of your project, and much more, by typing ./gradlew properties at the root of your project.

Add a new configuration at the end of build.gradle file:

configurations {
    integrationTestCompile.extendsFrom testCompile
    integrationTestRuntime.extendsFrom testRuntime
}

This will allow you to declare dependencies for integrationTestCompile and integrationTestRuntime. More importantly, by inheriting the test configurations, we have access to their dependencies.

Tip

I do not recommend declaring your integration test dependencies as integrationTestCompile. It will work as far as Gradle is concerned, but support inside of IDE is non-existent. What I usually do is declare my integration test dependencies as testCompile dependencies instead. This is only a small inconvenience.

Now that we have our new configurations, we must create a sourceSet class associated with them. A sourceSet class represents a logical group of Java source and resources. Naturally, they also have to inherit from the test and main classes; see the following code:

sourceSets {
    integrationTest {
        compileClasspath += main.output + test.output
        runtimeClasspath += main.output + test.output
    }
}

Finally, we need to add a task to run them from our build, as follows:

task integrationTest(type: Test) {
    testClassesDir = sourceSets.integrationTest.output.classesDir
    classpath = sourceSets.integrationTest.runtimeClasspath
    reports.html.destination = file("${reporting.baseDir}/integrationTests")
}

To run our test, we can type ./gradlew integrationTest. Besides configuring our classpath and where to find our test classes, we also defined a directory where the test report will be generated.

This configuration allows us to write our tests in src/integrationTest/java or src/integrationTest/groovy, which will make it easier to identify them and run them separately from our unit tests.

By default, they will be generated in build/reports/tests. If we do not override them, if we launch both tests and integration tests with gradle clean test integrationTest, they will override each other.

It's also worth mentioning that a young plugin in the Gradle ecosystem aims to simplify declaring new test configurations, visit https://plugins.gradle.org/plugin/org.unbroken-dome.test-sets for detailed information.

Our first FluentLenium test

FluentLenium is an amazing library for piloting Selenium tests. Let's add a few dependencies to our build script:

testCompile 'org.fluentlenium:fluentlenium-assertj:0.10.3'
testCompile 'com.codeborne:phantomjsdriver:1.2.1'
testCompile 'org.seleniumhq.selenium:selenium-java:2.45.0'

By default, fluentlenium comes with selenium-java. We redeclare it just to explicitly require the latest version available. We also added a dependency to the PhantomJS driver, which is not officially supported by Selenium. The problem with the selenium-java library is that it comes bundled with all the supported web drivers.

You can see the dependency tree of our project by typing gradle dependencies. At the bottom, you will see something like this:

+--- org.fluentlenium:fluentlenium-assertj:0.10.3
|    +--- org.fluentlenium:fluentlenium-core:0.10.3
|    |    --- org.seleniumhq.selenium:selenium-java:2.44.0 -> 2.45.0
|    |         +--- org.seleniumhq.selenium:selenium-chrome-driver:2.45.0

|    |         +--- org.seleniumhq.selenium:selenium-htmlunit-driver:2.45.0

|    |         +--- org.seleniumhq.selenium:selenium-firefox-driver:2.45.0

|    |         +--- org.seleniumhq.selenium:selenium-ie-driver:2.45.0

|    |         +--- org.seleniumhq.selenium:selenium-safari-driver:2.45.0

|    |         +--- org.webbitserver:webbit:0.4.14 (*)
|    |         --- org.seleniumhq.selenium:selenium-leg-rc:2.45.0
|    |              --- org.seleniumhq.selenium:selenium-remote-driver:2.45.0 (*)
|    --- org.assertj:assertj-core:1.6.1 -> 3.0.0

Having all those dependencies in the classpath is highly unnecessary since we will just use the PhantomJS driver. To exclude the dependencies we won't need, we can add the following part to our buildscript, right before the dependencies declaration:

configurations {
    testCompile {
        exclude module: 'selenium-safari-driver'
        exclude module: 'selenium-ie-driver'
        //exclude module: 'selenium-firefox-driver'
        exclude module: 'selenium-htmlunit-driver'
        exclude module: 'selenium-chrome-driver'
    }
}

We just keep the firefox driver at hand. PhantomJS driver is a headless browser, so understanding what happens without a GUI can prove tricky. It can be nice to switch to Firefox to debug a complex test.

With our classpath correctly configured, we can now write our first integration test. Spring Boot has a very convenient annotation to support this test:

import masterSpringMvc.MasterSpringMvcApplication;
import masterSpringMvc.search.StubTwitterSearchConfig;
import org.fluentlenium.adapter.FluentTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {
        MasterSpringMvcApplication.class,
        StubTwitterSearchConfig.class
})
@WebIntegrationTest(randomPort = true)
public class FluentIntegrationTest extends FluentTest {

    @Value("${local.server.port}")
    private int serverPort;

    @Override
    public WebDriver getDefaultDriver() {
        return new PhantomJSDriver();
    }

    public String getDefaultBaseUrl() {
        return "http://localhost:" + serverPort;
    }

    @Test
    public void hasPageTitle() {
        goTo("/");
        assertThat(findFirst("h2").getText()).isEqualTo("Login");
    }
}

Note that FluentLenium has a neat API for requesting DOM elements. With AssertJ, we can then write easy-to read-assertions on the page content.

Note

Have a look at the documentation at https://github.com/FluentLenium/FluentLenium for further information.

With the @WebIntegrationTest annotation, Spring will actually create the embedded Servlet container (Tomcat) and launch our web application on a random port! We need to retrieve this port number at runtime. This will allow us to provide a base URL for our tests, a URL that will be the prefix for all the navigation we do in our tests.

If you try to run the test at this stage, you will see the following error message:

java.lang.IllegalStateException: The path to the driver executable must be set by the phantomjs.binary.path capability/system property/PATH variable; for more information, see https://github.com/ariya/phantomjs/wiki. The latest version can be downloaded from http://phantomjs.org/download.html

Indeed, PhantomJS needs to be installed on your machine for this to work correctly. On a Mac, simply use brew install phantomjs. For other platforms, see the documentation at http://phantomjs.org/download.html.

If you don't want to install a new binary on your machine, replace new PhantomJSDriver() with new FirefoxDriver(). Your test will be a bit slower, but you will have a GUI.

Our first test is landing on the profile page, right? We need to find a way to log in now.

What about faking login with a stub?

Put this class in the test sources (src/test/java):

package masterSpringMvc.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInController;
import org.springframework.social.connect.web.SignInAdapter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.view.RedirectView;

@Configuration
public class StubSocialSigninConfig {

    @Bean
    @Primary
    @Autowired
    public ProviderSignInController signInController(ConnectionFactoryLocator factoryLocator,
                                                     UsersConnectionRepository usersRepository,
                                                     SignInAdapter signInAdapter) {
        return new FakeSigninController(factoryLocator, usersRepository, signInAdapter);
    }

    public class FakeSigninController extends ProviderSignInController {
        public FakeSigninController(ConnectionFactoryLocator connectionFactoryLocator,
                                    UsersConnectionRepository usersConnectionRepository,
                                    SignInAdapter signInAdapter) {
            super(connectionFactoryLocator, usersConnectionRepository, signInAdapter);
        }

        @Override
        public RedirectView signIn(String providerId, NativeWebRequest request) {
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken("geowarin", null, null);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            return new RedirectView("/");
        }
    }
}

This will authenticate any user clicking on the Twitter sign in button as geowarin.

We will write a second test that will fill the profile form and assert that the search result is displayed:

import masterSpringMvc.MasterSpringMvcApplication;
import masterSpringMvc.auth.StubSocialSigninConfig;
import masterSpringMvc.search.StubTwitterSearchConfig;
import org.fluentlenium.adapter.FluentTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.fluentlenium.core.filter.FilterConstructor.withName;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {
        MasterSpringMvcApplication.class,
        StubTwitterSearchConfig.class,
        StubSocialSigninConfig.class
})
@WebIntegrationTest(randomPort = true)
public class FluentIntegrationTest extends FluentTest {

    @Value("${local.server.port}")
    private int serverPort;

    @Override
    public WebDriver getDefaultDriver() {
        return new PhantomJSDriver();
    }

    public String getDefaultBaseUrl() {
        return "http://localhost:" + serverPort;
    }

    @Test
    public void hasPageTitle() {
        goTo("/");
        assertThat(findFirst("h2").getText()).isEqualTo("Login");
    }

    @Test
    public void should_be_redirected_after_filling_form() {
        goTo("/");
        assertThat(findFirst("h2").getText()).isEqualTo("Login");

        find("button", withName("twitterSignin")).click();
        assertThat(findFirst("h2").getText()).isEqualTo("Your profile");

        fill("#twitterHandle").with("geowarin");
        fill("#email").with("[email protected]");
        fill("#birthDate").with("03/19/1987");

        find("button", withName("addTaste")).click();
        fill("#tastes0").with("spring");

        find("button", withName("save")).click();

        takeScreenShot();
        assertThat(findFirst("h2").getText()).isEqualTo("Tweet results for spring");
        assertThat(findFirst("ul.collection").find("li")).hasSize(2);
    }
}

Note that we can easily ask our web driver to take a screenshot of the current browser used for testing. This will produce the following output:

Our first FluentLenium test

Page Objects with FluentLenium

The previous test was a bit messy. We have hardcoded all the selectors in our test. This can become very risky when we write a lot of tests using the same elements because whenever we change the page layout, all the tests will break. Moreover, the test is a little difficult to read.

To fix this, a common practice is to use a page object that will represent a specific web page in our application. With FluentLenium, page objects must inherit the FluentPage class.

We will create three pages, one for each element of our GUI. The first one will be the login page with the option to click on the twitterSignin button, the second one will be the profile page with convenience methods for filling in the profile form, and the last one will be the result page on which we can assert the results displayed.

Let's create the login page at once. I put all the three pages in a pages package:

package pages;

import org.fluentlenium.core.FluentPage;
import org.fluentlenium.core.domain.FluentWebElement;
import org.openqa.selenium.support.FindBy;

import static org.assertj.core.api.Assertions.assertThat;

public class LoginPage extends FluentPage {
    @FindBy(name = "twitterSignin")
    FluentWebElement signinButton;

    public String getUrl() {
        return "/login";
    }

    public void isAt() {
        assertThat(findFirst("h2").getText()).isEqualTo("Login");
    }

    public void login() {
        signinButton.click();
    }
}

Let's create one page for our profile page:

package pages;

import org.fluentlenium.core.FluentPage;
import org.fluentlenium.core.domain.FluentWebElement;
import org.openqa.selenium.support.FindBy;

import static org.assertj.core.api.Assertions.assertThat;

public class ProfilePage extends FluentPage {
    @FindBy(name = "addTaste")
    FluentWebElement addTasteButton;
    @FindBy(name = "save")
    FluentWebElement saveButton;

    public String getUrl() {
        return "/profile";
    }

    public void isAt() {
        assertThat(findFirst("h2").getText()).isEqualTo("Your profile");
    }

    public void fillInfos(String twitterHandle, String email, String birthDate) {
        fill("#twitterHandle").with(twitterHandle);
        fill("#email").with(email);
        fill("#birthDate").with(birthDate);
    }

    public void addTaste(String taste) {
        addTasteButton.click();
        fill("#tastes0").with(taste);
    }

    public void saveProfile() {
        saveButton.click();
    }
}

Let's also create another one for the search result page:

package pages;

import com.google.common.base.Joiner;
import org.fluentlenium.core.FluentPage;
import org.fluentlenium.core.domain.FluentWebElement;
import org.openqa.selenium.support.FindBy;

import static org.assertj.core.api.Assertions.assertThat;

public class SearchResultPage extends FluentPage {
    @FindBy(css = "ul.collection")
    FluentWebElement resultList;

    public void isAt(String... keywords) {
        assertThat(findFirst("h2").getText())
                .isEqualTo("Tweet results for " + Joiner.on(",").join(keywords));
    }

    public int getNumberOfResults() {
        return resultList.find("li").size();
    }
}

We can now refactor the test using those Page Objects:

@Page
private LoginPage loginPage;
@Page
private ProfilePage profilePage;
@Page
private SearchResultPage searchResultPage;

@Test
public void should_be_redirected_after_filling_form() {
    goTo("/");
    loginPage.isAt();

    loginPage.login();
    profilePage.isAt();

    profilePage.fillInfos("geowarin", "[email protected]", "03/19/1987");
    profilePage.addTaste("spring");

    profilePage.saveProfile();

    takeScreenShot();
    searchResultPage.isAt();
    assertThat(searchResultPage.getNumberOfResults()).isEqualTo(2);
}

Much more readable, isn't it?

Making our tests more Groovy

If you don't know Groovy, consider it like a close cousin of Java, without the verbosity. Groovy is a dynamic language with optional typing. This means that you can have the guarantees of a type system when it matters and the versatility of duck typing when you know what you are doing.

With this language, you can write POJOs without getters, setters, equals and hashcode methods. Everything is handled for you.

Writing == will actually call the equals method. The operators can be overloaded, which allows a neat syntax with little arrows, such as <<, to write text to a file, for instance. It also means that you can add integers to BigIntegers and get a correct result.

The Groovy Development Kit (GDK) also adds several very interesting methods to classic Java objects. It also considers regular expressions and closures as first-class citizens.

Note

If you want a solid introduction to Groovy, check out the Groovy style guide at http://www.groovy-lang.org/style-guide.html.

You can also watch this amazing presentation by Peter Ledbrook at http://www.infoq.com/presentations/groovy-for-java.

As far as I am concerned, I always try to push Groovy on the testing side of the application I work on. It really improves the readability of the code and the productivity of developers.

Unit tests with Spock

To be able to write Groovy tests in our project, we need to use the Groovy plugin instead of the Java plugin.

Here's what you have in your build script:

apply plugin: 'java'

Change it to the following:

apply plugin: 'groovy'

This modification is perfectly harmless. The Groovy plugin extends the Java plugin, so the only difference it makes is that it gives the ability to add Groovy source in src/main/groovy, src/test/groovy and src/integrationTest/groovy.

Obviously, we also need to add Groovy to the classpath. We will also add Spock, the most popular Groovy testing library, via the spock-spring dependency, which will enable compatibility with Spring:

testCompile 'org.codehaus.groovy:groovy-all:2.4.4:indy'
testCompile 'org.spockframework:spock-spring'

We can now rewrite HomeControllerTest with a different approach. Let's create a HomeControllerSpec class in src/test/groovy. I added it to the masterSpringMvc.controller package just like our first instance of HomeControllerTest:

package masterSpringMvc.controller

import masterSpringMvc.MasterSpringMvcApplication
import masterSpringMvc.search.StubTwitterSearchConfig
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import spock.lang.Specification

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ContextConfiguration(loader = SpringApplicationContextLoader,
        classes = [MasterSpringMvcApplication, StubTwitterSearchConfig])
@WebAppConfiguration
class HomeControllerSpec extends Specification {
    @Autowired
    WebApplicationContext wac;

    MockMvc mockMvc;

    def setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    def "User is redirected to its profile on his first visit"() {
        when: "I navigate to the home page"
        def response = this.mockMvc.perform(get("/"))

        then: "I am redirected to the profile page"
        response
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("/profile"))
    }
}

Our test instantaneously became more readable with the ability to use strings as method names and the little BDD DSL (Domain Specific Language) provided by Spock. This is not directly visible here, but every statement inside of a then block will implicitly be an assertion.

At the time of writing, because Spock doesn't read meta annotations, the @SpringApplicationConfiguration annotation cannot be used so we just replaced it with @ContextConfiguration(loader = SpringApplicationContextLoader), which is essentially the same thing.

We now have two versions of the same test, one in Java and the other in Groovy. It is up to you to choose the one that best fits your style of coding and remove the other one. If you decide to stick with Groovy, you will have to rewrite the should_redirect_to_tastes() test in Groovy. It should be easy enough.

Spock also has powerful support for mocks. We can rewrite the previous SearchControllerMockTest class a bit differently:

package masterSpringMvc.search

import masterSpringMvc.MasterSpringMvcApplication
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import spock.lang.Specification

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ContextConfiguration(loader = SpringApplicationContextLoader,
        classes = [MasterSpringMvcApplication])
@WebAppConfiguration
class SearchControllerMockSpec extends Specification {
    def twitterSearch = Mock(TwitterSearch)
    def searchController = new SearchController(twitterSearch)

    def mockMvc = MockMvcBuilders.standaloneSetup(searchController)
            .setRemoveSemicolonContent(false)
            .build()

    def "searching for the spring keyword should display the search page"() {
        when: "I search for spring"
        def response = mockMvc.perform(get("/search/mixed;keywords=spring"))

        then: "The search service is called once"
        1 * twitterSearch.search(_, _) >> [new LightTweet('tweetText')]

        and: "The result page is shown"
        response
                .andExpect(status().isOk())
                .andExpect(view().name("resultPage"))

        and: "The model contains the result tweets"
        response
                .andExpect(model().attribute("tweets", everyItem(
                hasProperty("text", is("tweetText"))
        )))
    }
}

All the verbosity of Mockito is now gone. The then block actually asserts that the twitterSearch method is called once (1 *) with any parameter (_, _). Like with mockito, we could have expected specific parameters.

The double arrow >> syntax is used to return an object from the mocked method. In our case, it's a list containing only one element.

With only a little dependency in our classpath, we have already written more readable tests, but we're not done yet. We will also refactor our acceptance tests to use Geb, a Groovy library that pilots Selenium tests.

Integration tests with Geb

Geb is the de facto library for writing tests in the Grails framework. Although its version is 0.12.0, it is very stable and extremely comfortable to work with.

It provides a selector API à la jQuery, which makes tests easy to write, even for frontend developers. Groovy is also a language that has some JavaScript influences that will also appeal to them.

Let's add Geb with the support for Spock specifications to our classpath:

testCompile 'org.gebish:geb-spock:0.12.0'

Geb can be configured via a Groovy script found directly at the root of src/integrationTest/groovy, called GebConfig.groovy:

import org.openqa.selenium.Dimension
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.phantomjs.PhantomJSDriver

reportsDir = new File('./build/geb-reports')
driver = {
        def driver = new FirefoxDriver()
    // def driver = new PhantomJSDriver()
    driver.manage().window().setSize(new Dimension(1024, 768))
    return driver
}

In this configuration, we indicate where Geb will generate its reports and which driver to use. Reports in Geb are an enhanced version of screenshots, which also contains the current page in HTML. Their generation can be triggered at any moment by calling the report function inside a Geb test.

Let's rewrite out first integration test with Geb:

import geb.Configuration
import geb.spock.GebSpec
import masterSpringMvc.MasterSpringMvcApplication
import masterSpringMvc.search.StubTwitterSearchConfig
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.boot.test.WebIntegrationTest
import org.springframework.test.context.ContextConfiguration

@ContextConfiguration(loader = SpringApplicationContextLoader,
        classes = [MasterSpringMvcApplication, StubTwitterSearchConfig])
@WebIntegrationTest(randomPort = true)
class IntegrationSpec extends GebSpec {

    @Value('${local.server.port}')
    int port

    Configuration createConf() {
        def configuration = super.createConf()
        configuration.baseUrl = "http://localhost:$port"
        configuration
    }

    def "User is redirected to the login page when not logged"() {
        when: "I navigate to the home page"
        go '/'
//        report 'navigation-redirection'

        then: "I am redirected to the profile page"
        $('h2', 0).text() == 'Login'
    }
}

For the moment, it is very similar to FluentLenium. We can already see the $ function, which will allow us to grab a DOM element via its selector. Here, we also state that we want the first h2 in the page by giving the 0 index.

Page Objects with Geb

Page objects with Geb are a real pleasure to work with. We will create the same page objects that we did previously so that you can appreciate the differences.

With Geb, the Page Objects must inherit from the geb.Page class. First, let's create the LoginPage. I suggest avoiding putting it in the same package as the previous one. I created a package called geb.pages:

package geb.pages

import geb.Page

class LoginPage extends Page {

    static url = '/login'
    static at = { $('h2', 0).text() == 'Login' }
    static content = {
        twitterSignin { $('button', name: 'twitterSignin') }
    }

    void loginWithTwitter() {
        twitterSignin.click()
    }
}

Then we can create the ProfilePage:

package geb.pages

import geb.Page

class ProfilePage extends Page {

    static url = '/profile'
    static at = { $('h2', 0).text() == 'Your profile' }
    static content = {
        addTasteButton { $('button', name: 'addTaste') }
        saveButton { $('button', name: 'save') }
    }

    void fillInfos(String twitterHandle, String email, String birthDate) {
        $("#twitterHandle") << twitterHandle
        $("#email") << email
        $("#birthDate") << birthDate
    }

    void addTaste(String taste) {
        addTasteButton.click()
        $("#tastes0") << taste
    }

    void saveProfile() {
        saveButton.click();
    }
}

This is basically the same page as before. Note the little << to assign values to an input element. You could also call setText on them.

The at method is completely part of the framework, and Geb will automatically assert those when you navigate to the corresponding page.

Let's create the SearchResultPage:

package geb.pages

import geb.Page

class SearchResultPage extends Page {
    static url = '/search'
    static at = { $('h2', 0).text().startsWith('Tweet results for') }
    static content = {
        resultList { $('ul.collection') }
        results { resultList.find('li') }
    }
}

It's a bit shorter, thanks to the ability to reuse previously defined content for the results.

With out the Page Object set up, we can write the test as follows:

import geb.Configuration
import geb.pages.LoginPage
import geb.pages.ProfilePage
import geb.pages.SearchResultPage
import geb.spock.GebSpec
import masterSpringMvc.MasterSpringMvcApplication
import masterSpringMvc.auth.StubSocialSigninConfig
import masterSpringMvc.search.StubTwitterSearchConfig
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.boot.test.WebIntegrationTest
import org.springframework.test.context.ContextConfiguration

@ContextConfiguration(loader = SpringApplicationContextLoader,
        classes = [MasterSpringMvcApplication, StubTwitterSearchConfig, StubSocialSigninConfig])
@WebIntegrationTest(randomPort = true)
class IntegrationSpec extends GebSpec {

    @Value('${local.server.port}')
    int port

    Configuration createConf() {
        def configuration = super.createConf()
        configuration.baseUrl = "http://localhost:$port"
        configuration
    }

    def "User is redirected to the login page when not logged"() {
        when: "I navigate to the home page"
        go '/'

        then: "I am redirected to the login page"
        $('h2').text() == 'Login'
    }

    def "User is redirected to its profile on his first visit"() {
        when: 'I am connected'
        to LoginPage
        loginWithTwitter()

        and: "I navigate to the home page"
        go '/'

        then: "I am redirected to the profile page"
        $('h2').text() == 'Your profile'
    }

    def "After filling his profile, the user is taken to result matching his tastes"() {
        given: 'I am connected'
        to LoginPage
        loginWithTwitter()

        and: 'I am on my profile'
        to ProfilePage

        when: 'I fill my profile'
        fillInfos("geowarin", "[email protected]", "03/19/1987");
        addTaste("spring")

        and: 'I save it'
        saveProfile()

        then: 'I am taken to the search result page'
        at SearchResultPage
        page.results.size() == 2
    }
}

My, what a beauty! You can certainly write your user stories directly with Geb!

With our simple tests, we only scratched the surface of Geb. There is much more functionality available, and I encourage you to read the Book of Geb, a very fine piece of documentation available at http://www.gebish.org/manual/current/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.239.41