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: <span id="munged">${munged}</span>
<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