构建一个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插件
  • 使用当前释出版本的servletAPI

war插件添加了providedCompileprovidedRuntime配制,类似于常规的Java应用,表示在本地需要的依赖但是不要添加到生成的webdemo.war文件里。

用于javawar插件的plugins语法。都不用添加版本号,因为这些插件都包括在Gradle的发行版里。

通过执行wrapper任务为项目生成一个Gradle包裹是很好的习惯:

$ gradle wrapper --gradle-version=4.6
:wrapper

这将会产生gradlewgradlew.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.javaservlet文件,包含下面的内容:

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.jspJSP页面。这里定义了在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);
    }
}

测试模仿了 HttpServletRequestHttpServletResponseRequestDispatcher类。用于doGet测试,一个使用StringWriterPrintWriter被创建了,模拟的请求对象被配制为当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。

results matching ""

    No results matching ""