Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kenneth A. Kousen - Making Java Groovy - 2014.pdf
Скачиваний:
47
Добавлен:
19.03.2016
Размер:
15.36 Mб
Скачать

270

CHAPTER 10 Building and testing web applications

try {

servlet.doGet(req, resp); assertEquals("Hello, Servlet!",

resp.getContentAsString().trim()); } catch (Exception e) {

e.printStackTrace();

}

}

}

The try/catch blocks do their best to bury the essence in ceremony, but the intent is clear. The method instantiates the servlet and mock objects representing the servlet request and servlet response classes, and then it invokes the doGet method on the servlet with the mocks as arguments. The good part is that the MockHttpServletResponse class has a method called getContentAsString, which captures the data written to the output writer in the servlet so it can be compared to the expected answer.

Note that the mock classes are being used not as Spring beans in the traditional sense (as they are in chapter 7 on Spring), but simply as an available API.

Unit-testing servlets is that simple, as illustrated in figure 10.3. Instantiate the servlet, provide it with whatever mock objects it needs, invoke the proper do method, and check the results. This example showed getContentAsString; additional tests in this chapter will illustrate two other convenient methods: getForwardedUrl and getRedirectedUrl. With these classes and methods available, no deployment to a servlet container is required.

Mock object

Mock object

doGet(HttpServletRequest, HttpServletResponse) {

...

response.getWriter().print(...);

}

Mock object

Figure 10.3 Servlet tests using Spring mocks. The Spring API provides mock classes for the request, response, and session, and captures outputs, forwards, and redirected URLs.

So far, however, I haven’t used Groovy at all. What does Groovy provide to make servlet development and testing easier? I’ll answer that in the next section.

Unit testing isn’t always enough, though. I’d like to prove that my application classes work in practice as well, so I want to do an integration test, too. That means I need a servlet container, some way to deploy my web application, and a way to trigger requests types other than simple GETs. That’s the subject of the next section.

10.3.2Integration testing with Gradle

Gradle is a build tool implemented in Groovy, which was discussed extensively in chapter 5 on build processes. Gradle uses Groovy builder syntax to specify repositories, library dependencies, and build tasks. Executing a build using one of the normal plugins (like the Groovy plugin used throughout this book) downloads any needed dependencies, compiles and tests the code, and prepares a final report of the results.

www.it-ebooks.info

Unitand integration-testing web components

271

One of the advantages of working with Gradle is its large variety of available plugins. In this chapter I’m working with web applications, and Gradle understands their structure as well as regular Java or Groovy applications. All you need to do is include the war plugin, and everything works. Even better, Gradle also includes a jetty plugin, which is designed for testing web applications.

Simply add the following line to a Gradle build:

apply plugin:'war'

The project will then use the default Maven structure of a web application. That means the web directory src/main/webapp will hold any view layer files, like HTML, CSS, and JavaScript. That directory will also contain the WEB-INF subdirectory, which contains the web deployment descriptor, web.xml. The source structure can be mapped any way you want, but for this section I’ll stick with the default Maven approach.

Consider a web application that holds HelloServlet from the previous section. The project layout is shown in figure 10.4.

At this stage, the Gradle build file is very simple, as shown in the following listing.

Figure 10.4 Web project layout. The integrationTest directories are discussed later in this chapter. The project has the standard Maven structure for a web application.

Listing 10.10 Gradle build file for web application, using the war plugin

apply plugin:'groovy' apply plugin:'war'

repositories { mavenCentral()

Gradle war plugin

}

def springVersion = '3.2.2.RELEASE'

dependencies {

groovy "org.codehaus.groovy:groovy-all:2.1.5" providedCompile 'javax.servlet:servlet-api:2.5' providedCompile 'javax.servlet.jsp:jsp-api:2.2'

Use but do not deploy

testCompile "junit:junit:4.10"

testCompile "org.springframework:spring-core:$springVersion" testCompile "org.springframework:spring-test:$springVersion"

}

www.it-ebooks.info

272

CHAPTER 10 Building and testing web applications

The listing includes the war plugin. As usual, dependencies come from Maven central. The dependent libraries include JUnit and the Spring API libraries used for unittesting. The interesting feature is the providedCompile dependency. That tells Gradle that the servlet and JSP APIs are required during compilation but not at deployment, because the container will provide them.

The war plugin really shines when it’s combined with the jetty plugin. Jetty is a lightweight, open source servlet container hosted by the Eclipse foundation.4 This makes it convenient for testing web applications, and Gradle includes a jetty plugin with the standard distribution.

10.3.3Automating Jetty in the Gradle build

To use Jetty in Gradle, you need to add the plugin dependency, but you also need to configure some settings:

apply plugin:'jetty'

httpPort = 8080 stopPort = 9451 stopKey = 'foo'

The httpPort variable is the port that Jetty will use for HTTP requests. Using 8080 is typical, because it’s the default port for both Tomcat and Jetty, but it’s certainly not required. The Jetty container will listen for shutdown requests on the stopPort, and the plugin will send the stopKey to Jetty when it’s time to shut down.

Adding the plugin and properties to the Gradle build enables three new tasks:

1jettyRun, which starts the server and deploys the application

2jettyRunWar, which creates a WAR file before deployment

3jettyStop, which stops the server

That’s helpful, but I want to automate the process of deploying my application so that I can run an integration test without human intervention. To make that happen, I need the jettyRun and jettyRunWar tasks to run in “daemon” mode, which means that after starting, control will be returned to the build so it can continue with other tasks.

Therefore, I add the following line to the build:

[jettyRun, jettyRunWar]*.daemon = true

Remember that the spread-dot operator (*.) in Groovy here means to set the daemon property on each element of the collection. Without the star, the dot operator would try to set the property on the collection itself, which wouldn’t work.

The test itself can then be defined as a private method in the build file and called from inside a Gradle task, as follows:

4 See www.eclipse.org/jetty/ for details.

www.it-ebooks.info

Unitand integration-testing web components

273

task intTest(type: Test, dependsOn: jettyRun) << { callServlets()

jettyStop.execute()

}

private void callServlet() {

String response = "http://localhost:$httpPort/HelloServlet/hello"

.toURL().text.trim()

assert response == 'Hello, Servlet!'

}

The intTest task is defined using the left-shift operator (<<), which is an alias for adding a doLast closure. In other words, this defines the task but doesn’t execute it. Because the task depends on the jettyRun task, jettyRun will be called first if this task is invoked. The task calls the private callServlet method, which converts a String to a URL, accesses the site, and compares the response to the expected value. Once the method completes, the intTest task tells Jetty to shut down, and I’m finished.

I can invoke the intTest task directly from the command line, but I’d rather make it part of my normal build process. To do that, I notice that in the directed acyclic graph (DAG, see chapter 5) formed by the Gradle build file, the next task after the test task is completed is called check.

That sounded way more complicated than it actually was. All I needed to do was run Gradle with the –m flag to keep it from actually executing, which gives the following output:

prompt> gradle -m build :compileJava SKIPPED :processResources SKIPPED :classes SKIPPED

:war SKIPPED :assemble SKIPPED

:compileTestJava SKIPPED :processTestResources SKIPPED :testClasses SKIPPED

:test SKIPPED

:check SKIPPED

:build SKIPPED

BUILD SUCCESSFUL

As you can see, the check task occurs right after the test task completes, and the intTest task doesn’t execute at all unless I call for it. To put my task into the process, I set it as a dependency of the check task:

check.dependsOn intTest

Now if I run the same build task again, the integration test runs at the proper time:

prompt> gradle -m build :compileJava SKIPPED :processResources SKIPPED :classes SKIPPED

:war SKIPPED

www.it-ebooks.info

274

CHAPTER 10 Building and testing web applications

:assemble SKIPPED

:jettyRun SKIPPED

:compileTestJava SKIPPED

:processTestResources SKIPPED

:testClasses SKIPPED

:intTest SKIPPED

:test SKIPPED

:check SKIPPED

:build SKIPPED

BUILD SUCCESSFUL

Note that the jettyRun task is also triggered before the tests. Now everything works the way I want.

From one perspective, this is quite a feat of engineering. The class structure in Gradle makes it easy to define new tasks, I can make sure my task runs at the proper time, and I can even embed the test as Groovy code right in my build file.

The problem, of course, is that I can embed the test as Groovy code right in my build file. That works in this instance, but doing business logic (even testing) in a build file can’t be a good long-term solution. Test cases aren’t part of a build; a build calls them. Inside the build, they’re hard to maintain and not easily reusable.

10.3.4Using an integration-test source tree

A good way to separate the testing infrastructure from the actual tests is to create a special source tree for it. That provides a convenient location for the tests, which will run automatically at the proper point in the build.

Gradle projects have a sourceSets property, which can be used to map source directories if they don’t fit the default Maven pattern. An example of this was given in chapter 5. Here I want to add an additional testing directory. For both the Java and Groovy plugins, simply defining a source set name generates the proper tasks.

In the current build I add a source set called integrationTest:

sourceSets { integrationTest

}

This causes Gradle to generate tasks called compileIntegrationTestJava, compileIntegrationTestGroovy, processIntegrationTestResources, and integrationTestClasses. The directory tree now includes src/integrationTest/java, src/integrationTest/ groovy, and src/integrationTest/resources.

For this source set I would like the compile and runtime dependencies to match their counterparts in the regular test directory:

dependencies {

// ... Various libraries ...

integrationTestCompile configurations.testCompile integrationTestRuntime configurations.testRuntime

}

www.it-ebooks.info

GET request with HTTPBuilder

Unitand integration-testing web components

275

As before, I’ll use the intTest task, but now I need to configure it to have the proper classpath and test directories. Here’s the new version of the task:

task intTest(type: Test, dependsOn: jettyRun) {

testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath jettyStop.execute()

}

The testClassesDir property points to the compiled test sources. The classpath is set to the runtime classpath of the source set, which is simply the runtime classpath of the regular tests. I can now place integration tests into the src/integrationTest directory tree, and they’ll be executed at the proper time.

One final issue remains before presenting the integration tests. It’s easy to create an HTTP GET request: you convert the string URL to an instance of java.net.URL and then access its text property, as shown previously. It’s not as simple to create POST, PUT, and DELETE requests, however. These are discussed in some detail in chapter 8, but for now I’ll use a third-party open source library.

The HTTPBuilder library (http://groovy.codehaus.org/modules/http-builder/) is a Groovy wrapper around Apache’s HttpClient library. It uses Groovy to make it easy to execute HTTP requests and process the responses. To use it, I added the following dependency to my Gradle build file:

testCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.6'

With this addition, the following listing now shows a variety of integration tests. The test class includes tests both with the HTTPBuilder client and without.

Listing 10.11 ServletIntegrationTests.groovy: accessing deployed servlets

class ServletIntegrationTests { def httpPort = 8080

@Test

void testHelloServlet() { GET request String response =

"http://localhost:$httpPort/HelloServletWithHttpBuilder/hello"

.toURL().text.trim()

assert response == 'Hello, Servlet!'

}

@Test

void testHelloServletGetWithHttpBuilder() {

def http = new HTTPBuilder("http://localhost:$httpPort/")

def resp = http.get(path:'HelloServletWithHttpBuilder/hellogs', contentType: ContentType.TEXT) { resp, reader -> reader.text.trim()

}

assert resp == 'Hello from a Groovy Servlet!'

}

www.it-ebooks.info

276

CHAPTER 10 Building and testing web applications

 

 

 

@Test

 

POST request

 

 

 

void testHelloServletPostWithName() {

 

 

with HTTPBuilder

 

 

 

 

def http = new HTTPBuilder("http://localhost:$httpPort/")

 

 

def resp = http.post(path:'HelloServletWithHttpBuilder/hellogs', requestContentType: ContentType.TEXT,

query:[name:'Dolly']) { resp, reader -> reader.text.trim()

}

assert resp == 'Hello, Dolly!'

}

}

The listing demonstrates three different types of tests. The first shows a simple GET request without any library dependencies. The second uses the HTTPBuilder5 library to execute a GET request, and the last does the same with a POST request. The detailed syntax comes from the library documentation.

With this infrastructure in place, both unit and integration tests can be added to a standard project tree, and both can be executed with an embedded Jetty server using a plugin in the Gradle build.

GRADLE INTEGRATION TESTS Using Gradle’s web and jetty plugins with an integration source tree, web applications can be tested in “live” mode during a normal build.67

The Geb web testing framework

Geb (www.gebish.org) (pronounced “jeb,” with a soft g) is a Groovy testing tool based on Spock that allows tests to be written using a page-centric approach to web applications. Website interactions can be scripted in terms of page objects, rather than simple screen scraping. It uses a jQuery-like syntax along with Groovy semantics to do browser automation, using the WebDriver library under the hood.

The Geb project shows a lot of promise and has a growing number of adherents. It’s certainly worth considering as a functional testing tool, along with alternatives like Canoo WebTest (http://webtest.canoo.com) and the Selenium6 (http://seleniumhq.org) JavaScript library. An entire chapter could be written covering those tools alone, but this book is arguably already long enough.

Because this an active area of development, I recommend the testing presentations at Slide Share by Paul King (for example, www.slideshare.net/paulk_asert/make- tests-groovy), one of the coauthors of Groovy in Action (Manning, 2007) and an outstanding developer, as a helpful reference.7

5HTTPBuilder includes a class called RESTClient, which is used extensively in the discussion of REST in chapter 9.

6By the way, do you know why it’s called Selenium? When it was developed, there was a much-loathed product called Mercury Test Runner. As it happens, the element Selenium (Se) is the cure for Mercury (Hg) poisoning.

7I’ll just say it here: everything Paul King says is right. Start with that assumption and you’ll be fine.

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]