构建一个Java Web应用
Gradle包含了一个war
插件用于构建Java web应用,并且社区提供了极好的插件叫作gretty
用于测试和在Jetty或Tomcat中部署。这份指南展示了怎样构建一个简单的web应用和使用gretty
插件部署在Jettry上。你也应该学习怎么用Mockito框架写用于测试servlet的单元测试,和使用gretty
和Selenium写用于web应用的功能测试。
你将需要
- 大约21分钟
- 一个文本编译器或IDE
- 一个大于或高于7的Java发行版
- 一个Gradle等于或高于4.6的发行版
创建web应用的结构
Gradle包含了一个war
插件文档在用户手册的Web应用快速开始和WAR插件章节。war
插件拓展了Java插件添加了对web应用的支持。默认的,使用src/main/webapp
用于web相关的资源。
所以,为webdemo
的项目创建下面的文件结构:
简单的项目布局
webdemo/
src/
main/
java/
webapp/
test
java/
任务servlet和其他的Java类都在src/main/java
,测试类在src/text/java
,其它的web资源在src/main/webapp
添加Gradle构建文件
在项目的根目录添加一个名为build.gradle
的文件,这个文件包含下面的这些文件:
build.gradle
plugins {
id 'war' (1)
}
repositories {
jcenter()
}
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0' (2)
testCompile 'junit:junit:4.12'
}
- 使用
war
插件 - 使用当前释出版本的
servlet
API
war
插件添加了providedCompile
和providedRuntime
配制,类似于常规的Java应用,表示在本地需要的依赖但是不要添加到生成的webdemo.war
文件里。
用于java
和war
插件的plugins
语法。都不用添加版本号,因为这些插件都包括在Gradle的发行版里。
通过执行wrapper
任务为项目生成一个Gradle包裹是很好的习惯:
$ gradle wrapper --gradle-version=4.6
:wrapper
这将会产生gradlew
和gradlew.bat
脚本和包含wrapper jar文件的gradle文件夹(在用户手册 wrapper 一章介绍)
如果你使用Gradle4.0之后的版本,你在命令行看到的输出会比此指南的少。在此指南中,是在命令行中使用了
--console=plain
来显示输出的。这样作是为了展示Gradle执行了什么。
添加一个Servlet和元信息到项目
这有两个选项来定义web应用的元信息。对于servlet应用3.0之前的版本,元信息在项目的WEB-INF
文件夹称为布署描述符的web.xml
文件里。在3.0之后,元信息可以使用注解定义。
在src/main/java
文件夹下创建org/gradle/demo
包。添加HelloServlet.java
servlet文件,包含下面的内容:
src/main/java/org/gradle/demo/HelloServlet.java
package org.gradle.demo;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "HelloServlet", urlPatterns = {"hello"}, loadOnStartup = 1) (1)
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().print("Hello, World!"); (2)
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name = request.getParameter("name");
if (name == null) name = "World";
request.setAttribute("user", name);
request.getRequestDispatcher("response.jsp").forward(request, response); (3)
}
}
- 基于注解的servlet
- GET请求返回一个简单的字符串
- POST请求转发到JSP页面
servlet使用@WebServlet
注解用于配制。响应HTTP GET请求的doGet
方法向字符输出流中写入"Hello, World!"。相应的HTTP POST请求会寻找称为name
的请求参数并把它作为user
添加到请求属性中,然后转发到response.jsp
页面
war
插件支持使用老的web.xml
部署描述符,默认情况下其应该保留在WEB-INF
下的src/main/webapp
下。可以自由地使用它来替代基于注解的方法。
你现在拥有一个可以响应HTTP GET和POST请求的简单的servlet
向示例应用添加JSP页面
src/main/webapp/index.html
<html>
<head>
<title>Web Demo</title>
</head>
<body>
<p>Say <a href="hello">Hello</a></p> (1)
<form method="post" action="hello"> (2)
<h2>Name:</h2>
<input type="text" id="say-hello-text-input" name="name" />
<input type="submit" id="say-hello-button" value="Say Hello" />
</form>
</body>
</html>
- 点击提交一个GET请求
- 点击提交一个POST请求
index.html
页面使用链接提交一个HTTP GET请求,一个表单提交一个HTTP POST请求。表单包含了一个被称为name
的文本域,其访问到servlet的doPost
方法。
在它的doPost
方法中,servlet转发控制到被称为response.jsp
JSP页面。这里定义了在src/main/webapp
具有如下内容的文件
src/main/webapp/response.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello Page</title>
</head>
<body>
<h2>Hello, ${user}!</h2>
</body>
</html>
response
页面从request中访问user
变量并在h2
标签中渲染。
添加gretty
插件并运行
gretty
插件是一个由社区支持的优秀的插件可以在https://plugins.gradle.org/plugin/org.akhikhl.gretty。这个插件使在Jetty或Tomcat上运行和测试web应用变得更加简单。
添加下面的代码块到build.gradle
文件里,就把此插件添加到了我们的项目里了。
更新build.gradle
添加gretty
插件
plugins {
id 'war'
id 'org.akhikhl.gretty' version '1.4.2'
}
- 添加
gretty
插件
gretty
插件添加了大量的任务到应用中,这对于在Jetty或Tomcat环境上运行或测试很有用。现在你可以构建和部署应用到默认的容器(Jetty),通过使用appRun
任务。
执行appRun
任务
$ ./gradlew appRun
:prepareInplaceWebAppFolder
:createInplaceWebAppFolder UP-TO-DATE
:compileJava
:processResources UP-TO-DATE
:classes
:prepareInplaceWebAppClasses
:prepareInplaceWebApp
:appRun
12:25:13 INFO Jetty 9.2.15.v20160210 started and listening on port 8080
12:25:13 INFO webdemo runs at:
12:25:13 INFO http://localhost:8080/webdemo
Press any key to stop the server.
> Building 87% > :appRun
BUILD SUCCESSFUL
你可以通过http://localhost:8080/webdemo访问此web应用,既可以点击链接执行一个GET请求或提交表单执行一个POST请求。
仅管输出说Press any key to stop the server
,标准的输入不会打断Gradle。停止此程序,按ctrl-C
。
使用Mockito进行单元测试
开源的Mockito
框架使对Java进行单元测试变得很容易。在build.gradle
文件下的testCompile
配制项添加Mockito
添加Mockito库到build.gradle
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.7.19' (1)
}
- 添加Mockito
对servlet进行单元测试,在src/test/java
创建org.gradle.demo.beneath
包。添加一个具有下面内容的测试类:
src/test/java/org/gradle/demo/HelloServletTest.java
package org.gradle.demo;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;
public class HelloServletTest {
@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private RequestDispatcher requestDispatcher;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public void doGet() throws Exception {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
when(response.getWriter()).thenReturn(printWriter);
new HelloServlet().doGet(request, response);
assertEquals("Hello, World!", stringWriter.toString());
}
@Test
public void doPostWithoutName() throws Exception {
when(request.getRequestDispatcher("response.jsp"))
.thenReturn(requestDispatcher);
new HelloServlet().doPost(request, response);
verify(request).setAttribute("user", "World");
verify(requestDispatcher).forward(request,response);
}
@Test
public void doPostWithName() throws Exception {
when(request.getParameter("name")).thenReturn("Dolly");
when(request.getRequestDispatcher("response.jsp"))
.thenReturn(requestDispatcher);
new HelloServlet().doPost(request, response);
verify(request).setAttribute("user", "Dolly");
verify(requestDispatcher).forward(request,response);
}
}
测试模仿了 HttpServletRequest
,HttpServletResponse
,RequestDispatcher
类。用于doGet
测试,一个使用StringWriter
的PrintWriter
被创建了,模拟的请求对象被配制为当getWriter
方法被调用时返回此打印流。在调用doGet
方法之后,测试会检查返回的方法是否是正确的。
对于post
请求,模拟的request对象被配制为返回给定的名字如果存在的话否则返回null,getRequestDispatcher
返回相关联的模拟对象。调用doPost
方法执行请求。Mockito会核实调用了setAttribute
方法并在request dispatcher中调用了forward
方法。
你现在可以使用Gradle的test
任务来测试servlet(或其他依赖它的任务,像build
)。
$ ./gradlew build
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:war
:assemble
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:check
:build
BUILD SUCCESSFUL
可以从build/reports/tests/test/index.html
访问测试的输出。在通常情况下,你应该获得像下面的内容:
添加一个功能性的测试
与Gradle结合的gretty
插件使测试web应用变得很容易。作这样的测试,添加下面的行到你的build.gradle
文件:
在build.gradle
, Gretty额外的用于功能性测试的配制
gretty {
integrationTestTask = 'test' (1)
}
// ... rest from before ...
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.7.19'
testCompile 'io.github.bonigarcia:webdrivermanager:1.6.1' (2)
testCompile 'org.seleniumhq.selenium:selenium-java:3.3.1' (3)
}
- 告诉gretty在测试时启动或停止服务
- 自动安装浏览器驱动
- 使用Selenium用于功能测试
gretty
插件需要知道哪些任务需要启动和停止服务。通常你需要赋值给你自己的任务,但你可以使用已经存在的test
任务。
Selenium是一个非常流行的用于功能性测试的开源软件API。2.0版本基于WebDriver API。最近的版本需要测试者下载和安装他们的浏览器的WebDriver版本,这样会是很枯燥和很难自动化的。WebDriverManager项目使Gradle处理这些问题很容易
为你的项目添加功能性测试,在src/text/java
目录:
src/test/java/org/gradle/demo/HelloServletFunctionalTest.java
package org.gradle.demo;
import io.github.bonigarcia.wdm.ChromeDriverManager;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import static org.junit.Assert.assertEquals;
public class HelloServletFunctionalTest {
private WebDriver driver;
@BeforeClass
public static void setupClass() {
ChromeDriverManager.getInstance().setup(); (1)
}
@Before
public void setUp() {
driver = new ChromeDriver(); (2)
}
@After
public void tearDown() {
if (driver != null)
driver.quit(); (3)
}
@Test
public void sayHello() throws Exception { (4)
driver.get("http://localhost:8080/webdemo");
driver.findElement(By.id("say-hello-text-input")).sendKeys("Dolly");
driver.findElement(By.id("say-hello-button")).click();
assertEquals("Hello Page", driver.getTitle());
assertEquals("Hello, Dolly!", driver.findElement(By.tagName("h2")).getText());
}
}
- 下载并安装流星器驱动,如果必要的话
- 开始浏览器自动化测试
- 当完成测试的时候关闭浏览器
- 使用Selenium API运行功能性测试
WebDriverManager执行此测试检测驱动最近的二进制版本,如果不存在的话下载安装。然后sayHello
测试方法驱动Chrome浏览器来打开我们应用的主页,在文本框中填入广西,点击按钮,然后核对目标页面的标题和h2
标签包含期盼的字符串。
WebDriverManager系统支持Chrome,Opera,Internet Explorer,Mircrosoft Edge,PhantomJS,和 Firefox。检出项目的文档来获取更详细的信息。
运行功能测试
使用test
任务运行测试
$ ./gradlew test
:prepareInplaceWebAppFolder UP-TO-DATE
:createInplaceWebAppFolder UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:prepareInplaceWebAppClasses UP-TO-DATE
:prepareInplaceWebApp UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:appBeforeIntegrationTest
12:57:56 INFO Jetty 9.2.15.v20160210 started and listening on port 8080
12:57:56 INFO webdemo runs at:
12:57:56 INFO http://localhost:8080/webdemo
:test
:appAfterIntegrationTest
Server stopped.
BUILD SUCCESSFUL
gretty
插件以默认的端口启动嵌入版本的Jetty9,执行测试,并且停止服务。如果你在观察,你会看到Selenium系统打开了一个新的浏览器,访问网站,完成表单,点击按钮,检查新的页面,最后关掉浏览器。
集成测试经常创建一系列分开的源码和任务,但这超出了本指南的范围。参阅Gretty文档获取更详细的信息。
总结
在这篇指南中,你学会了怎样:
- 在Gradle构建中使用
war
插件来定义web应用 - 在web应用中添加servlet和JSP页面
- 使用
gretty
插件来部署应用 - 使用Mockito来测试一个servlet
- 使用
gretty
和Selenium来进行功能性测试
下一步
Gretty具有非常强大的API。参阅Gretty文档获取更详细的信息。更多关于Selenium的信息可以在Selenium网站获取,更多关于WebDriverManager系统的信息可以在 WebdriverDriverManager GitHub 仓库找到。
如果你对功能性测试更感兴趣,检出Geb库,提供了用于浏览器测试更加强大的Groovy DSL语法-基于Selenium和WebDriver。