讀古今文學網 > 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測試的細節。