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 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.
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.
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.
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:
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); }
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.
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.
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.
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 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/.