映月读书网 > Spring Boot实战 > 4.3 测试运行中的应用程序 >

4.3 测试运行中的应用程序

说到测试Web应用程序,我们还没接触实质内容。在真实的服务器里启动应用程序,用真实的Web浏览器访问它,这样比使用模拟的测试引擎更能展现应用程序在用户端的行为。

但是,用真实的Web浏览器在真实的服务器上运行测试会很麻烦。虽然构建时的插件能把应用程序部署到Tomcat或者Jetty里,但它们配置起来多有不便。而且测试这么多,几乎不可能隔离运行,也很难不启动构建工具。

然而Spring Boot找到了解决方案。它支持将Tomcat或Jetty这样的嵌入式Servlet容器作为运行中的应用程序的一部分,可以运用相同的机制,在测试过程中用嵌入式Servlet容器来启动应用程序。

Spring Boot的@WebIntegrationTest注解就是这么做的。在测试类上添加@WebIntegrationTest注解,可以声明你不仅希望Spring Boot为测试创建应用程序上下文,还要启动一个嵌入式的Servlet容器。一旦应用程序运行在嵌入式容器里,你就可以发起真实的HTTP请求,断言结果了。

举例来说,考虑一下代码清单4-5里的那段简单的Web测试。这里采用@WebIntegrationTest,在服务器里启动了应用程序,以Spring的RestTemplate对应用程序发起HTTP请求。

代码清单4-5 测试运行在服务器里的Web应用程序

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(
      classes=ReadingListApplication.class)
@WebIntegrationTest            ←---在服务器里运行测试
public class SimpleWebTest {

  @Test(expected=HttpClientErrorException.class)
  public void pageNotFound {
    try {
      RestTemplate rest = new RestTemplate;
      rest.getForObject(
           \"http://localhost:8080/bogusPage\", String.class);   ←---发起GET请求
      fail(\"Should result in HTTP 404\");
    } catch (HttpClientErrorException e) {
      assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode);   ←---判断HTTP 404(not found)响应
      throw e;
    }
  }

}

  

虽然这个测试非常简单,但足以演示如何使用@WebIntegrationTest在服务器里启动应用程序。要判断实际启动的服务器究竟是哪个,可以遵循在命令行里运行应用程序时的逻辑。默认情况下,会有一个监听8080端口的Tomcat启动。但是,如果Classpath里有的话,Jetty或者Undertow也能启动这些服务器。

测试方法的主体部分假设应用程序已经运行,监听了8080端口。它使用了Spring的RestTemplate对一个不存在的页面发起请求,判断服务器的响应是否为HTTP 404(NOT FOUND)。如果返回了其他响应,则测试失败。

4.3.1 用随机端口启动服务器

前面提到过,此处的默认行为是启动服务器监听8080端口。在一台机器上一次只运行一个测试的话,这没什么问题,因为没有其他服务器监听8080端口。但如果你和我一样,本机总是有其他服务器在监听8080端口,那该怎么办?这时测试会失败,因为端口冲突,服务器启动不了。一定要有更好的办法才行。

幸运的是,让Spring Boot在随机选择的端口上启动服务器很方便。一种办法是将server.port属性设置为0,让Spring Boot选择一个随机的可用端口。@WebIntegrationTestvalue属性接受一个String数组,数组中的每项都是键值对,形如name=value,用来设置测试中使用的属性。要设置server.port,你可以这样做:

@WebIntegrationTest(value={\"server.port=0\"})

  

另外,因为只要设置一个属性,所以还能有更简单的形式:

@WebIntegrationTest(\"server.port=0\")

  

通过value属性来设置属性通常还算方便。但@WebIntegrationTest还提供了一个randomPort属性,更明确地表示让服务器在随机端口上启动。你可以将randomPort设置为true,启用随机端口:

@WebIntegrationTest(randomPort=true)

  

既然我们在随机端口上启动了服务器,就需要在发起Web请求时确保使用正确的端口。此时的getForObject方法在URL里硬编码了8080端口。如果端口是随机选择的,那在构造请求时又该怎么确定正确的端口呢?

首先,我们需要以实例变量的形式注入选中的端口。为了方便,Spring Boot将local.server.port的值设置为了选中的端口。我们只需使用Spring的@Value注解将其注入即可:

@Value(\"${local.server.port}\")
private int port;

  

有了端口之后,只需对getForObject稍作修改,使用这个port就好了:

rest.getForObject(
    \"http://localhost:{port}/bogusPage\", String.class, port);

  

这里我们在URL里把硬编码的8080改为{port}占位符。在getForObject调用里把port属性作为最后一个参数传入,就能确保该占位符被替换为注入port的值了。

4.3.2 使用Selenium测试HTML页面

RestTemplate对于简单的请求而言使用方便,是测试REST端点的理想工具。但是,就算它能对返回HTML页面的URL发起请求,也不方便对页面内容或者页面上执行的操作进行断言。结果HTML里的内容最好能够精确判断(这种测试很脆弱)。不过你无法轻易判断页面上选中的内容,或者执行诸如点击链接或提交表单这样的操作。

对于HTML应用程序测试,有一个更好的选择——Selenium(www.seleniumhq.org),它的功能远不止提交请求和获取结果。它能实际打开一个Web浏览器,在浏览器的上下文中执行测试。Selenium尽量接近手动执行测试,但与手工测试不同。Selenium的测试是自动的,而且可以重复运行。

为了用Selenium测试阅读列表应用程序,让我们先写一个测试来获取首页,为新书填写表单,提交表单,随后判断返回的页面里是否包含新添加的图书。

首先需要把Selenium作为测试依赖添加到项目里:

testCompile(\"org.seleniumhq.selenium:selenium-java:2.45.0\")

  

现在就可以编写测试了。代码清单4-6是一个基本的Selenium测试模板,使用了Spring Boot的@WebIntegrationTest

代码清单4-6 在Spring Boot里使用Selenium测试的模板

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(
      classes=ReadingListApplication.class)
@WebIntegrationTest(randomPort=true)      ←---用随机端口启动
public class ServerWebTests {

  private static FirefoxDriver browser;

  @Value(\"${local.server.port}\")     ←---注入端口号
  private int port;

  @BeforeClass
  public static void openBrowser {
    browser = new FirefoxDriver;
    browser.manage.timeouts
        .implicitlyWait(10, TimeUnit.SECONDS);    ←---配置Firefox驱动
  }

  @AfterClass
  public static void closeBrowser {
    browser.quit;     ←---关闭浏览器
  }

}

  

和之前更简单的Web测试一样,这个类添加了@WebIntegrationTest注解,将randomPort设置为true,这样应用程序启动后会运行一个监听随机端口的服务器。同样,端口号注入port属性,这样我们就能用它来构造指向运行中应用程序的URL了。

静态方法openBrowser会创建一个FirefoxDriver的实例,它将打开Firefox浏览器(需要在运行测试的服务器上安装该浏览器)。我们的测试方法将通过FirefoxDriver实例来执行浏览器操作。在页面上查找元素时,FirefoxDriver配置了10秒的等候时间(以防元素加载过慢)。

测试执行完毕,我们需要关闭Firefox浏览器。因此要在closeBrowser里要调用FirefoxDriver实例的quit方法,关闭浏览器。

选择浏览器 虽然我们用Firefox进行了测试,但Selenium还提供了不少其他浏览器的驱动,包括IE、Google的Chrome,还有Apple的Safari。测试可以使用其他浏览器。你也可以使用你想支持的各种浏览器,这也许也是个不错的想法。

现在可以开始编写测试方法了,给你提个醒,我们想要加载首页,填充并发送表单,然后判断登录的页面是否包含刚刚添加的新书。代码清单4-7演示了如何用Selenium实现这个功能。

代码清单4-7 用Selenium测试阅读列表应用程序

@Test
public void addBookToEmptyList {
  String baseUrl = \"http://localhost:\" + port;

  browser.get(baseUrl);        ←---获取主页

  assertEquals(\"You have no books in your book list\",
               browser.findElementByTagName(\"p\").getText);   ←---判断图书列表是否为空

  browser.findElementByName(\"title\")
         .sendKeys(\"BOOK TITLE\");
  browser.findElementByName(\"author\")
         .sendKeys(\"BOOK AUTHOR\");
  browser.findElementByName(\"isbn\")
         .sendKeys(\"1234567890\");
  browser.findElementByName(\"description\")
         .sendKeys(\"DESCRIPTION\");
  browser.findElementByTagName(\"form\")
         .submit;       ←---填充并发送表单

  WebElement dl =
      browser.findElementByCssSelector(\"dt.bookHeadline\");
  assertEquals(\"BOOK TITLE by BOOK AUTHOR (ISBN: 1234567890)\",
               dl.getText);
  WebElement dt =
      browser.findElementByCssSelector(\"dd.bookDescription\");
  assertEquals(\"DESCRIPTION\", dt.getText);     ←---判断列表中是否包含新书
}

 

该测试方法所做的第一件事是使用FirefoxDriver来发起GET请求,获取阅读列表的主页,随后查找页面里的一个<p>元素,从它的文本里判断列表里没有图书。

接下来的几行查找表单里的元素,使用驱动的sendKeys方法模拟敲击键盘事件(实际上就是用给定的值填充那些表单域)。最后,找到<form>元素并提交。

提交的表单经处理后,浏览器就会跳到一个页面,上面的列表包含了新添加的图书。因此最后几行查找列表里的<dt><dd>元素,判断其中是否包含测试表单里提交的数据。

运行测试时,你会看到浏览器打开,加载阅读列表应用程序。如果够仔细,你还会看到填充表单的过程,就好像幽灵在操作,当然,并没有幽灵使用你的应用程序——这只是一个测试。

这个测试里最值得注意的是,@WebIntegrationTest可以为我们启动应用程序和服务器,这样Selenium才可以用Web浏览器执行测试。但真正有趣的是你可以使用IDE的测试功能来运行测试,运行几次都行,无需依赖构建过程中的某些插件启动服务器。

要是你觉得使用Selenium进行测试很实用,可以阅读Yujun Liang和Alex Collins的Selenium WebDriver in Practice(http://manning.com/liang/),该书更深入地讨论了Selenium测试的细节。