Cucumber JVM: Web Application with Spring MVC

It’s been a while since the last Cucumber JVM post, and since I’ve started to work with Java based web applications, it was time to continue the series. Last time I left off, I had a class that could transform an arbitrary sentence such as “I like testing” into “I lkie tnitseg” by following the rules of the text munger kata. For building and running my application, I’m going to use tomcat and spring MVC, because they are just enough to put together a decent web application.

Preparation

This is what I want my web application to do:

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

I’ll need two more files besides src/test/resources/web_text_munger.feature from above. First, WebTextMunger_Test.java which is supposed to load that feature file and run the scenarios:

@RunWith(Cucumber.class)
@Cucumber.Options(features="classpath:web_text_munger.feature")
public class WebTextMunger_Test {
}

Second, WebTextMungerStepsdef.java which contains the step definitions using selenium for browser based testing:

public class WebTextMungerStepsdef {
  private WebDriver browser = new FireFoxDriver();

  @Given("^I am on the home page")
  public void I_am_on_the_home_page() {
    browser.get(tomcat.getApplicationUrl("munger"));
  }

  @When("^I enter the \"([^\"]*)\"$")
  public void I_enter_(String text) {
    browser.findElement(By.id("text")).sendKeys(text);
  }

  @Then("^I see \"([^\"]*)\" as munged text$")
  public void I_see_as_munged_text(String text) {
    assertEquals(text, browser.findElement(By.id("munged")).getText());
  }

  // More steps
}

Tomcat

This test case fails, because there isn’t any web service running. It’s good that I have examples on how to use embedded web services in JUnit, so that I can use EmbeddedTomcat from the blog.embedded.webservice project. It runs very nicely, but there are some dependency issues: spring-web comes with the 2.5 version of javax.servlet, but Tomcat 7 does not work well with that version, because it requires at least 3.x. If you have both versions on the classpath - which is the case now -, you have a good chance that nothing will work. In order to avoid this situation you need to exclude the default javax.servlet implementations:

<ivy-module version="2.0">
  <!-- ... -->
  <dependencies>
    <!-- other dependencies -->

    <dependency org="org.apache.tomcat.embed" name="tomcat-embed-core" rev="7.0.28"/>
    <dependency org="org.apache.tomcat.embed" name="tomcat-embed-jasper" rev="7.0.28"/>
    <dependency org="org.apache.tomcat.embed" name="tomcat-embed-logging-juli" rev="7.0.28"/>
    <dependency org="org.apache.tomcat" name="tomcat-jsp-api" rev="7.0.28" />

    <!-- the dependency trick -->
    <dependency org="javax.servlet.jsp.jstl" name="javax.servlet.jsp.jstl-api" rev="1.2.1" />
    <exclude org="javax.servlet" />
    <exclude org="javax.servlet.jsp" />
    <exclude org="javax.el" />
  </dependencies>
</ivy-module>

Spring MVC

#### The Controller Spring offers a very nice way of using the Model View Controller (MVC) pattern. Let’s start with the controller. In order to make it work, the HTTP calls have to go through the so called DispatcherServlet, which has to be set up in the web.xml file:

<web-app>
  <servlet>
  <servlet-name>munger</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
  <servlet-name>munger</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

I want all the requests go through that servlet, that’s why I’m having the <url-pattern>/</url-pattern> as it is. When an HTTP call arrives at the DispatcherServlet, it tries to find the proper controller among the registered ones that can process that request using one of its actions. The controller registration is done by adding the @Controller annotation to the class, and the actions are marked by the @RequestMapping annotation - MungerController.java:

package com.zsoltfabok.blog.web.controller;

@Controller
public class MungerController {

  @RequestMapping("/")
  public String show() {
    return "index";
  }

  @RequestMapping(value = "/munge", method=RequestMethod.POST)
  public String munge(@RequestParam("text") String text, Model model) {
    model.addAttribute(/* key */, /* value */);
    return "index";
  }
}

So, when a POST request on the ‘/munger/munge’ URI arrives, first it is sent to the DispatcherServlet, which is looking for a matching @RequestMapping, and will find the munge() method from above. It will call the method and pass the text parameter from the request to the method as an argument - @RequestParam("text"). The method does something and when it is done it returns the name of the view that has to be rendered in response to the request - "index" in this case - along with the attributes the view can use: model.addAttribute(/* key */, /* value */). In case of a different URI or request method the DispatcherServlet will generate an error message and will render a 404 page. An exception, of course, is the ‘/’ URI which is mapped in the controller (show()).

In order to be able to use the controller, I’ll need a so-called <servlet_name>-servlet.xml file, which is very similar to an applicationContext.xml. That file must be named after the name of the servlet and must be in the WEB-INF folder right next to web.xml, otherwise it won’t work (this is how Spring MVC works):

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

  <mvc:annotation-driven />
  <context:component-scan base-package="com.zsoltfabok.blog" />

  <bean id="viewResolver"
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix">
      <value>/WEB-INF/views/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </bean>
</beans>

Additionally, it also defines how the view files are found. If I’m returning "index" from my controller, it will look for a file in the /WEB-INF/views folder with the name "index" with the extension .jsp.

The View

The view is pretty simple:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page isELIgnored="false" %>
<html>
  <head>
    <title>App</title>
  </head>
  <body>
    <form name="input" action="munge" method="post">
      your text:
      <input id="text" type="text" name="text" />
      <input id="submit" type="submit" value="Submit" />
    </form>
    <c:if test="${munged != null}">
      munged:&nbsp;<span id="munged">${munged}</span>&nbsp;
      <span id="original">(${original})</span>
    </c:if>
  </body>
</html>

The id attributes are very important here, because I’m using them to find certain elements on the page when running the steps (for example: browser.findElement(By.id("munged")).getText()). I could have used some CSS selector or classes, but at that moment I felt that the id based approach is enough.

You may wonder what the <%@ page isELIgnored="false" %> line does. No matter what I did with this file, the <c:if /> part wasn’t evaluated until I added that line. In case your are interested in the problem in more detail, have a look a this stackoverflow question and answer. The previously mentioned model values can be reached from the <c:> nodes using the ${name} format. Note that using this format outside <c:> nodes will simply render to ${name}.

The Model and the Business Logic

In Spring MVC the model has nothing to do with databases and business logic as in Ruby on Rails. It is simply used to transfer data between the view and the controller. However, it was very easy to connect the existing business logic to the controller by simply adding an annotated private reference to the controller - MungerController.java:

@Controller
public class MungerController {

  @Autowired
  private SimpleTextMunger munger;

  @RequestMapping(value = "/munge", method=RequestMethod.POST)
  public String munge(@RequestParam("text") String text, Model model) {
    model.addAttribute("munged", munger.execute(text));
    model.addAttribute("original", text);
    return "index";
  }
}

When the controller needs a reference to the business logic it will simply instantiate the munger - along with its dependencies - and call the execute(String) method. I really like this approach: now the controller is thin and the business logic is clearly separated from the representation layout (view) and usage (controller).

Connecting the Dots

Now, I have the test scenario, I have an embedded web service and the application itself. Putting them together can easily be done with some additional steps:

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 the "flow flow"
      And I press "submit"
     Then I see "folw folw" as munged text
      And I see "flow flow" as the original
      And I close the browser                 # NEW (VERY BAD, DO NOT DO THIS HERE!)

Although this is working, it is not a nice solution. Starting the web service, the deployment and the browser have nothing to do with the scenario or the business case it describes, hence they shouldn’t be there like this. Cucumber has a nice feature called hooks which are used to execute something before and after the scenarios. Hooks are exactly the right place where the web service startup, the deployment and the browser startup should be. I’m going to write more about hooks in the next post of this series, but until that you can find the source for this post under the episode_5 branch of the repository on github.


comments powered by Disqus