Integrating Cucumber and Play

October 13, 2013
Play! Play Testing Cucumber Behaviour Driven Development BDD

The default application generated by Play has examples of unit and integration tests, however it does not demonstrate how to integrate in any Behaviour Driven Development (BDD) frameworks. The value of a BDD framework like Cucumber is extremely high when attempting to achieve strong collaboration between various stakeholders like Developers and Business Analysts. This post will show you how to get up and running with Cucumber and Play. The example uses Play 2.2.0 and Java.

  1. Assuming you have already setup a vanilla Play application already, the first thing you need to do is add the dependencies for Cucumber. Go to your build.sbt file and add in the dependencies for Cucumber. In this post, we will be using cucumber-jvm, along with the cucumber-junit launcher.

    name := "sample-with-cucumber"
    
    version := "1.0-SNAPSHOT"
    
    libraryDependencies ++= Seq(
      javaJdbc,
      javaEbean,
      cache
      "info.cukes" % "cucumber-java" % "1.1.5" % "test",
      "info.cukes" % "cucumber-junit" % "1.1.5" % "test"
     )     
    
    play.Project.playJavaSettings
    
    
  2. Let's create a directory to contain our feature files. I generally put my features in a features directory in the root of my application.

    $ mkdir features
    
  3. For this directory to be picked up, we need to add it into the classpath when running tests. This is a change we need to make to the build.sbt file.

    name := "sample-with-cucumber"
    
    version := "1.0-SNAPSHOT"
    
    libraryDependencies ++= Seq(
      javaJdbc,
      javaEbean,
      cache
      "info.cukes" % "cucumber-java" % "1.1.5" % "test",
      "info.cukes" % "cucumber-junit" % "1.1.5" % "test"
     )     
    
    play.Project.playJavaSettings
    
    unmanagedResourceDirectories in Test <+= baseDirectory( _ / "features" )
    
    
  4. To keep everything simple, let's create a very basic feature file. All this will do is go to the landing page and assert the title of the page is equal to the text "Cucumber".

    Create a file features/example.feature containing the following contents.

    Feature: Testing Cucumber Integration
    
      Scenario: Cucumber Integration
        Given I have setup Play
        When I go to the landing page
        Then the title should be "Cucumber"
    
    
  5. To get this running, we need a class that is annotated with the Cucumber JUnit runner annotation. So create a class called test/RunCucumber.java with the following contents. The "pretty" flag is optional, but I much prefer that output.

    import cucumber.api.junit.Cucumber;
    import org.junit.runner.RunWith;
    
    @RunWith(Cucumber.class)
    @Cucumber.Options(format = {"pretty"})
    public class RunCucumber {
    }
    
  6. Now let's checkpoint what we have done so far. If everything is working, we should be able to run Cucumber and it should complain that we are missing step definitions.

    $ play test
    
    ...
    You can implement missing steps with the snippets below:
    
    @Given("^I have setup Play$")
    public void I_have_setup_Play() throws Throwable {
        // Express the Regexp above with the code you wish you had
        throw new PendingException();
    }
    
    @When("^I go to the landing page$")
    public void I_go_to_the_landing_page() throws Throwable {
        // Express the Regexp above with the code you wish you had
        throw new PendingException();
    }
    
    @Then("^the title should be \"([^\"]*)\"$")
    public void the_title_should_be(String arg1) throws Throwable {
        // Express the Regexp above with the code you wish you had
        throw new PendingException();
    }
    
    
  7. To get the scenario to pass, we need to first put a startup hook in place to initialize our server and test browser. Cucumber does provide a @Before hook but this is executed before each and every scenario. We only want to initialize our server and browser once before the very first scenario, so we need to add some state to manage this ourselves.

    Create a test/GlobalHooks.java class and add the following contents.

    import cucumber.api.java.Before;
    import play.test.TestBrowser;
    import play.test.TestServer;
    import static play.test.Helpers.*;
    
    public class GlobalHooks {
        public static int PORT = 3333;
        public static TestBrowser TEST_BROWSER;
        private static TestServer TEST_SERVER;
        private static boolean initialised = false;
    
        @Before
        public void before() {
            if (!initialised) {
                TEST_SERVER = testServer(PORT, fakeApplication(inMemoryDatabase()));
                TEST_BROWSER = testBrowser(HTMLUNIT, PORT);
                start(TEST_SERVER);
                initialised = true;
            }
        }
    
    }
    

    There's also no shutdown hook in Cucumber, so we need to add the clean up of the server and browser into the JVM shutdown hook.

    import cucumber.api.java.Before;
    import play.test.TestBrowser;
    import play.test.TestServer;
    import static play.test.Helpers.*;
    
    public class GlobalHooks {
        public static int PORT = 3333;
        public static TestBrowser TEST_BROWSER;
        private static TestServer TEST_SERVER;
        private static boolean initialised = false;
    
        @Before
        public void before() {
            if (!initialised) {
                TEST_SERVER = testServer(PORT, fakeApplication(inMemoryDatabase()));
                TEST_BROWSER = testBrowser(HTMLUNIT, PORT);
                start(TEST_SERVER);
                initialised = true;
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        TEST_BROWSER.quit();
                        TEST_SERVER.stop();
                    }
                });
            }
        }
    }
    
  8. Now we should be able to implement our steps. So create a test/Steps.java file. For the moment, we will just statically reference our TEST_BROWSER. There is a better way to inject this dependency in, which I will cover later.

    import cucumber.api.java.en.Given;
    import cucumber.api.java.en.Then;
    import cucumber.api.java.en.When;
    import static org.fest.assertions.Assertions.assertThat;
    
    public class Steps {
        @Given("^I have setup Play$")
        public void I_have_setup_Play() throws Throwable {
        }
    
        @When("^I go to the landing page$")
        public void I_go_to_the_landing_page() throws Throwable {
            GlobalHooks.TEST_BROWSER.goTo("http://localhost:" + GlobalHooks.PORT);
        }
    
        @Then("^the title should be \"([^\"]*)\"$")
        public void the_title_should_be(String title) throws Throwable {
            assertThat(GlobalHooks.TEST_BROWSER.title()).isEqualTo(title);
        }
    }
    
  9. We should now be in a position to rerun this. The steps should be found however the title of the page will not be "Cucumber" resulting in a failure.

    $ play test
    
    ...
    0m[error] Test Scenario: Cucumber Integration failed: expected:<'[Cucumber]'> but was:<'[Welcome to Play]'>
    ...
    
  10. Let's correct the title of the page and rerun the test.

    Navigate to app/views/index.scala.html. Update the title of the page to be "Cucumber"

    @(message: String)
    
    @main("Cucumber") {
        @play20.welcome(message, style = "Java")
    }
    
  11. Rerun the test and it should succeed.

    1 Scenarios (1 passed)
    3 Steps (3 passed)
    0m3.544s
    
  12. The last series of changes will be around integrating with Guice so we can inject our dependencies into the Steps. This will remove the need to directly reference the test browser.

    Go back into the build.sbt file and add the dependencies for guice as well as cucumber-guice, which provides the binding for guice into cucumber.

    name := "sample-with-cucumber"
    
    version := "1.0-SNAPSHOT"
    
    libraryDependencies ++= Seq(
      javaJdbc,
      javaEbean,
      cache,
      "com.google.inject" % "guice" % "3.0" % "test",
      "info.cukes" % "cucumber-guice" % "1.1.5" % "test",
      "info.cukes" % "cucumber-java" % "1.1.5" % "test",
      "info.cukes" % "cucumber-junit" % "1.1.5" % "test"
     )     
    
    play.Project.playJavaSettings
    
    unmanagedResourceDirectories in Test <+= baseDirectory( _ / "features" )
    
  13. Define a test/CucumberModule.java class which will setup our bindings.

    import com.google.inject.AbstractModule;
    import com.google.inject.name.Names;
    import play.test.TestBrowser;
    import play.test.TestServer;
    import static play.test.Helpers.*;
    
    public class CucumberModule extends AbstractModule {
    
        private static int PORT = 3333;
        private TestServer testServer = testServer(PORT, fakeApplication(inMemoryDatabase()));
        private TestBrowser testBrowser = testBrowser(HTMLUNIT, PORT);
    
        @Override
        protected void configure() {
            bind(TestBrowser.class).toInstance(testBrowser);
            bind(TestServer.class).toInstance(testServer);
            bind(Integer.class).annotatedWith(Names.named("PORT")).toInstance(PORT);
        }
    }
    
    

    Now change test/GlobalHooks.java to be dependency injected.

    import com.google.inject.Inject;
    import cucumber.api.java.Before;
    import play.test.TestBrowser;
    import play.test.TestServer;
    import static play.test.Helpers.*;
    
    public class GlobalHooks {
        @Inject
        private TestBrowser testBrowser;
        @Inject
        private TestServer testServer;
        private static boolean initialised = false;
    
        @Before
        public void before() {
            if (!initialised) {
                start(testServer);
                initialised = true;
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        testBrowser.quit();
                        testServer.stop();
                    }
                });
            }
        }
    }
    

    Change test/Steps.java to be dependency injected.

    import com.google.inject.Inject;
    import com.google.inject.name.Named;
    import cucumber.api.java.en.Given;
    import cucumber.api.java.en.Then;
    import cucumber.api.java.en.When;
    import play.test.TestBrowser;
    import static org.fest.assertions.Assertions.assertThat;
    
    public class Steps {
        @Inject
        private TestBrowser testBrowser;
    
        @Inject
        @Named("PORT")
        private Integer port;
    
        @Given("^I have setup Play$")
        public void I_have_setup_Play() throws Throwable {
        }
    
        @When("^I go to the landing page$")
        public void I_go_to_the_landing_page() throws Throwable {
            testBrowser.goTo("http://localhost:" + port);
        }
    
        @Then("^the title should be \"([^\"]*)\"$")
        public void the_title_should_be(String title) throws Throwable {
            assertThat(testBrowser.title()).isEqualTo(title);
        }
    }
    
  14. Lastly add a features.cucumber-guice.properties file to refer to our module. So this file should have the following contents.

    guiceModule=CucumberModule
    
  15. Now if we rerun everything, then the test should still pass which will prove that everything has been setup correctly.

    $ play test
    
    Feature: Testing Cucumber Integration
    
      Scenario: Cucumber Integration        # example.feature:3
        Given I have setup Play             # Steps.I_have_setup_Play()
        When I go to the landing page       # Steps.I_go_to_the_landing_page()
        Then the title should be "Cucumber" # Steps.the_title_should_be(String)
    
    1 Scenarios (1 passed)
    3 Steps (3 passed)
    0m2.092s
    

So this posting has taken you through the steps required to integrate Cucumber and Play. It also has shown you how to integrate Guice into your Cucumber Steps to keep your code cleaner. An example of this sample application can be found here.