Cucumber JVM: Hooks

I finished the previous post with the promise that I’m going write more about hooks in cucumber. By definition, hooks allow us to perform actions at various points in the cucumber test cycle. The definition wisely doesn’t say much about these actions and the points where they are executed. They wary from performing prerequisite actions for scenarios to sending emails when a certain step fails. One can ask, What these have to do with the following scenario - src/test/resources/web_text_munger.feature:

Feature: web text munger kata
  Scenario: It should process a sentence
    Given the embedded tomcat is running      # NEW
      And the application is deployed         # NEW
      And I am using Firefox for testing      # NEW
      And I am on the home page
     When I enter "flow flow"
      And I press "submit"
     Then I see "folw folw" as the munged text
      And I see "flow flow" as the original
      And I close the browser                 # NEW (VERY BAD, DO NOT DO THIS HERE!)

The steps marked with # NEW have nothing to do with the business case of the scenario. They are prerequisites that I need done if I want to run the scenario. For these, I need a different kind of approach: I’ll call these steps in different hooks.

Scenario Hooks

Scenario hooks can be defined with the cucumber.annotation.Before and cucumber.annotation.After annotations (JUnit has the exact same annotations in the org.junit package, make sure you use the right ones as cucumber will not process JUnit annotations. Don’t try to use JUnit’s @BeforeClass and @AfterClass annotations either for the same reason – see also section about global hooks):

import cucumber.annotation.After;
import cucumber.annotation.Before;

@Before
public void beforeScenario() {
  tomcat.start();
  tomcat.deploy("munger");
}

@After
public void afterScenario() {
  tomcat.stop();
}

Let’s remove the unnecessary lines from the web_text_munger.feature and move their content to the scenario hooks:

Feature: web text munger kata
  Scenario: It should process a sentence
    Given I am on the home page
     When I enter the "flow flow"
      And I press "submit"
     Then I see "folw folw" as munged text
      And I see "flow flow" as the original

The WebTextMungerStepsdef.java:

@Before
public void beforeScenario() {
    tomcat.start();
    tomcat.deploy("munger");
    browser = new FirefoxDriver();
}

@After
public void afterScenario() {
    browser.close();
    tomcat.stop();
}

The scenario executes without any problems and it seems that we are done. Now let’s run the old simple_text_munger.feature as well. No problem there either, but a Firefox window just popped up unexpectedly. It seems that the before hook has been executed twice. The methods annotated with @Before and @After are executed before and after every scenario regardless of where they are defined. That’s not good for us right now.

Tagged Hooks

It is possible to define a hook so that it is only executed before or after scenarios that are tagged with a specified annotation, for example:

Feature: web text munger kata
  @web
  Scenario: It should process a sentence
    // The steps

The WebTextMungerStepsdef.java:

@Before("@web")
public void beforeScenario() {
  // actions
}

@After("@web")
public void afterScenario() {
  // actions
}

It executes nicely and the test case is green. Let’s add another scenario to the web_text_munger.feature. Both are green, no problems here, but the Firefox window popped up twice again. Since both of the scenarios have been tagged, the hooks have been executed twice: once for each scenario. If you have a lot of scenarios, it’s worth executing the before and after hooks only once in order to save time and resources.

Global Hooks: The Hacking

Unfortunately, cucumber doesn’t support global hooks at the moment. It is possible to solve this situation, but it is an ugly hack. Let’s use the first @web annotation to start the web service and the browser - WebTextMungerStepsdef.java:

private static EmbeddedTomcat tomcat = new EmbeddedTomcat();
private static WebDriver browser;

@Before("@web")
public void beforeScenario() {
    if (!tomcat.isRunning()) {
        tomcat.start();
        tomcat.deploy("munger");
        browser = new FirefoxDriver();
    }
}

Now the tomcat and browser references have nice global states, that’s why I don’t really like this solution, but I don’t have a better one at the moment, so I’ll try to live with it.

Closing the browser and stopping the web service is a bit trickier, we don’t know which the last scenario that requires them is, so we should shut them down when the test suite exits. This solution is pretty much the same as in ruby because there the at_exit is proposed to be used in cases like this - WebTextMungerStepsdef.java:

@Before("@web")
public void beforeScenario() {
    if (!tomcat.isRunning()) {
        tomcat.start();
        tomcat.deploy("munger");
        browser = new FirefoxDriver();

        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                browser.close();
                tomcat.stop();
            }
        });
    }
}

Final Words

Don’t let the previous example discourage you from using hooks. They provide a great opportunity for setting up scenarios and cleaning up afterwards. The code is available under the episode_6 branch on github. Happy testing!


comments powered by Disqus