Spring 學習筆記:Spring 測試框架

Spring 測試框架

主要內容:spring 對集成測試和單元測試的支持。

Spring測試框架具有特性的意義:

  • 讓TDD編程更加容易;
  • 可以在不啓動服務器的情況下進行測試, 會比啓動真實的服務器更加快;
  • 可以模擬出依賴的服務, 讓測試更加專注, 比如說模擬出service層, 讓測試專注於controller層;
  • 對一些狀態存儲在外部系統(如數據庫), 更加快速的做存根(通過bean定義);
  • 需要有完整數據依賴的測試,模擬服務層或者dao層, 是使用測試數據庫之外的一個選擇。

相較於端到端測試, 是否只是快了一些?
初次之外, 因爲是執行在一個程序裏的, 可以斷言web應用中方法執行情況

單元測試

單元測試的優點是測試關注點集中, 運行速度塊, 但是缺點是配置複雜, 需要對一個測試的目標類的依賴的類進行mock。

模擬對象

Spring包含許多專用於模擬的軟件包:

  • Environment
  • JNDI
  • Servlet API
  • Spring Web Reactive

Environment

該org.springframework.mock.env軟件包包含Environment和PropertySource抽象的模擬實現 (請參閱 Bean定義配置文件 和PropertySource抽象)。 MockEnvironment和MockPropertySource可以爲需要依賴容器環境的單元測試提供mock環境。

JNDI

該org.springframework.mock.jndi軟件包包含JNDI SPI的實現,可用於爲測試套件或獨立應用程序設置簡單的JNDI環境。例如,如果JDBC DataSource實例在測試代碼中與在Java EE容器中綁定到相同的JNDI名稱,則可以在測試場景中重用應用程序代碼和配置,而無需進行修改。

Servlet API

該org.springframework.mock.web軟件包包含一套全面的Servlet API模擬對象,可用於測試Web上下文,控制器和過濾器。這些模擬對象是針對Spring的Web MVC框架使用的,並且通常比使用EasyMock, MockObjects等模擬框架更加方便。

Spring MVC測試框架建立在模擬Servlet API對象的基礎上,爲Spring MVC提供了集成測試。

Spring Web reactive

org.springframework.mock.http.server.reactive包含WebFlux應用程序的mock實現,ServerHttpRequest和ServerHttpResponse。org.springframework.mock.web.server包含一個ServerWebExchange依賴於上面那些模擬請求和響應對象的模擬。
MockServerHttpRequest並MockServerHttpResponse從相同的抽象基類繼承, 在基類裏定義了一些共同的行爲。例如,模擬請求一旦創建便是不可變的,但是可以使用mutate()方法ServerHttpRequest來創建修改後的實例。
爲了使模擬響應能夠正確實現寫約定並返回寫完成句柄(即Mono<Void>),默認情況下,它使用Fluxwith cache().then()來緩衝數據並使其可用於測試中的斷言。應用程序可以設置自定義寫入功能(例如,測試無限流)。
可以使用WebTestClient測試WebFlux應用, 這種測試不需要運行真正的服務器。這個client還可以用於對真正運行的服務器進行端到端測試。

單元測試支持類庫

Spring包含許多可以幫助進行單元測試的類。它們分爲兩類:

  • 通用測試工具類
  • Spring MVC測試工具類

通用測試工具類

該org.springframework.test.util軟件包包含幾個用於單元和集成測試的通用工具類。
ReflectionTestUtils是基於反射的工具方法的集合。有設置非public字段,調用非public設置方法或調用非public生命週期回調方法的功能。如下:
縱容private或protected直接訪問的ORM框架(例如JPA和Hibernate)。
Spring的註解(如支持@Autowired,@Inject和@Resource),用在private或protected字段,方法時。
當使用@PostConstruct和@PreDestroy註解非公開方法時。

Spring MVC 測試工具類

org.springframework.test.web包包含ModelAndViewAssert,可以將其與JUnit,TestNG或任何其他測試框架結合使用,以進行處理Spring MVC ModelAndView對象的單元測試。

要對作爲POJO的Spring MVC Controller類進行單元測試,請將ModelAndViewAssert與Spring的Servlet API模擬中的MockHttpServletRequest,MockHttpSession等結合使用。
完整的集成測試, 使用Spring MVC集成測試框架。

集成測試

本節涵蓋了Spring應用程序的集成測試。包括以下主題:

  • 概覽
  • 集成測試的目標
  • JDBC 測試支持
  • 註解
  • Spring TestContext框架
  • Spring MVC測試框架

概覽

能夠執行一些集成測試而無需部署到應用程序服務器或連接到其他的組件是一種很重要的測試能力。 這可以讓我們測試:

  • Spring 容器中各個bean是否正確關聯起來了
  • 數據訪問層, 如sql語句是否正確

Spring框架爲spring-test模塊中的集成測試提供了一流的支持。
此類測試的運行速度比單元測試慢,但比依賴於部署到應用程序服務器的等效Selenium測試或遠程測試快。

單元和集成測試支持以註釋驅動的Spring TestContext Framework的形式提供。 TestContext框架與實際使用的測試框架無關,該框架允許在各種底層測試框架(包括JUnit,TestNG和其他環境)上運行。

Spring集成測試框架的目標

有以下的主要目標:

  • 在不同測試用例之間管理Spring IoC容器緩存。
  • 提供依賴注入。
  • 提供適合集成測試的事務管理。
  • 提供特定於Spring的基類,可以提供如ApplicationContext和jdbcTemplate等實例, 可以簡化測試用例的編寫

JDBC測試支持

org.springframework.test.jdbc的JdbcTestUtils, 包含了以下一些支持測試的靜態方法:

  • countRowsInTable(…)
  • countRowsInTableWhere(…);
  • deleteFromTables(…);
  • deleteFromTableWhere(…);
  • dropTables(…);

測試相關的註解

Spring Testing註解

以下註解在所有底層具體的測試框架都被支持:

  • @BootstrapWith
  • @ContextConfiguration
  • @WebAppConfiguration
  • @ContextHierarchy
  • @ActiveProfiles
  • @TestPropertySource
  • @DirtiesContext
  • @TestExecutionListeners
  • @Commit
  • @Rollback
  • @BeforeTransaction
  • @AfterTransaction
  • @Sql
  • @SqlConfig
  • @SqlGroup

@BootstrapWith

@BootstrapWith是一個類級別的註解,可用於配置如何引導Spring TestContext Framework。具體來說,可以使用@BootstrapWith指定自定義TestContextBootstrapper.

@ContextConfiguration

類級別的註解@ContextConfiguration聲明應用容器配置文件或java配置類的位置, 用於確定如何加載和配置用於集成測試的ApplicationContext。

資源位置通常是位於類路徑中的XML配置文件或Groovy腳本,而帶註解的類通常是@Configuration類。但是,資源位置也可以引用文件系統中的文件和腳本,帶註解的類可以是@Component類.

一個指向XML文件的@ContextConfiguration註解示例:

@ContextConfiguration("/test-config.xml") 
public class XmlApplicationContextTests {
    // class body...
}

指向java配置類的示例:

@ContextConfiguration(classes = TestConfig.class) 
public class ConfigClassApplicationContextTests {
    // class body...
}

除了聲明資源位置或帶註釋的類以外,還可以使用@ContextConfiguration聲明ApplicationContextInitializer類:

@ContextConfiguration(initializers = CustomContextIntializer.class) 
public class ContextInitializerTests {
    // class body...
}

可以選擇使用@ContextConfiguration來聲明ContextLoader策略(一般不需要, xml和java的加載就已經足夠):

@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) 
public class CustomLoaderXmlApplicationContextTests {
    // class body...
}

@WebAppConfiguration

@WebAppConfiguration是一個類級別的註解,可用於聲明爲集成測試加載的ApplicationContext應該是WebApplicationContext。 @WebAppConfiguration僅存在於測試類上,可以確保爲測試加載WebApplicationContext,並使用默認值“ file:src / main / webapp”作爲Web應用程序根目錄(即資源)的路徑。資源基礎路徑用於在後臺創建MockServletContext,該MockServletContext用作測試的WebApplicationContext的ServletContext。
示例:

@ContextConfiguration
@WebAppConfiguration 
public class WebAppTests {
    // class body...
}

指定web環境的資源路徑:

@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") 
public class WebAppTests {
    // class body...
}

支持classpath和file前綴, 默認爲file路徑。
注意,@ WebAppConfiguration必須在單個測試類中或在測試類層次結構中與@ContextConfiguration結合使用

支持classpath和file前綴, 默認爲file路徑。
注意,@ WebAppConfiguration必須在單個測試類中或在測試類層次結構中與@ContextConfiguration結合使用

@ContextHierarchy

@ContextHierarchy是一個類級別的批註,用於定義用於集成測試的ApplicationContext實例的層次結構。示例:

@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
public class ContextHierarchyTests {
    // class body...
}

@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = AppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
public class WebIntegrationTests {
    // class body...
}

@ActiveProfiles

@ActiveProfiles是一個類級別的註釋,用於聲明集成測試中啓用的profile。
啓用dev profile的示例:

@ContextConfiguration
@ActiveProfiles("dev") 
public class DeveloperTests {
    // class body...
}

啓用多個profile:

@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) 
public class DeveloperIntegrationTests {
    // class body...
}

啓用多個profile:

@TestPropertySource

@TestPropertySource是一個類級別的批註,您可以使用它來配置屬性文件和內聯屬性, 這些屬性將在測試時被加載到ApplicationContext中。

這種方式加載的屬性優先級高於從操作系統環境或Java系統屬性以及應用程序通過@PropertySource聲明性地或以編程方式添加的屬性源中加載的屬性。因此,測試屬性源可用於選擇性覆蓋系統和應用程序屬性源中定義的屬性。此外,內聯屬性比從資源位置加載的屬性具有更高的優先級。

從類路徑加載屬性的示例:

@ContextConfiguration
@TestPropertySource("/test.properties") 
public class MyIntegrationTests {
    // class body...
}

加載內聯屬性:

@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) 
public class MyIntegrationTests {
    // class body...
}

@DirtiesContext

@DirtiesContext表示底層的Spring ApplicationContext在執行測試期間已被弄髒(即,該測試以某種方式修改或破壞了它,例如,通過更改單例bean的狀態),應將其關閉。當應用程序上下文標記爲髒時,會將其從測試框架的緩存中刪除並關閉。因此,將爲需要上下文具有相同配置元數據的任何後續測試重建基礎Spring容器。
可以將@DirtiesContext用作類或類層次結構中的類級別和方法級別的註釋。

進入這個測試類之前, 準備一個新的容器環境:

@DirtiesContext(classMode = BEFORE_CLASS) 
public class FreshContextTests {
    // some tests that require a new Spring container
}

執行完這個測試類之後, 準備一個新的容器環境:

@DirtiesContext 
public class ContextDirtyingTests {
    // some tests that result in the Spring container being dirtied
}

在進入每個方法之前, 準備一個新的容器環境

@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) 
public class FreshContextTests {
    // some tests that require a new Spring container
}

在退出每個方法之後, 準備一個新的容器環境

@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) 
public class ContextDirtyingTests {
    // some tests that result in the Spring container being dirtied
}

在進入這個方法之前, 準備一個新的容器環境

@DirtiesContext(methodMode = BEFORE_METHOD) 
@Test
public void testProcessWhichRequiresFreshAppCtx() {
    // some logic that requires a new Spring container
}

在退出這個方法之後, 準備一個新的容器環境

@DirtiesContext 
@Test
public void testProcessWhichDirtiesAppCtx() {
    // some logic that results in the Spring container being dirtied
}

默認情況下, 準備新的容器環境會移除所有配置, 可以指定移除一部分配置:

@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
public class BaseTests {
    // class body...
}

public class ExtendedTests extends BaseTests {

@Test
@DirtiesContext(hierarchyMode = CURRENT_LEVEL) 
public void test() {
    // some logic that results in the child context being dirtied
}

}

@TestExecutionListeners

可以通過註冊監聽器, 在測試的各個階段執行回調。
示例:

@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) 
public class CustomTestExecutionListenerTests {
    // class body...
}

@Commit

@Commit指示應在測試方法完成後提交用於事務性測試方法的事務。可以將@Commit用作@Rollback(false)的直接替代品,以更明確地傳達代碼的意圖。與@Rollback類似,@ Commit也可以聲明爲類級別或方法級別的註釋。
示例:

@Commit 
@Test
public void testProcessWithoutRollback() {
    // ...
}

@Rollback

@Rollback指示在測試方法完成後是否應回退用於事務性測試方法的事務。如果爲true,則回滾該事務。否則,將提交事務(另請參見@Commit)。即使未明確聲明@ Rollback,Spring TestContext Framework中用於集成測試的回滾默認爲true, 可以在類級別和方法級別定義。

@Rollback(false) 
@Test
public void testProcessWithoutRollback() {
    // ...
}

@BeforeTransaction

在執行事務之前執行。 示例:

@BeforeTransaction 
void beforeTransaction() {
    // logic to be executed before a transaction is started
}

@AfterTransaction

在執行事務之後執行。 示例:

@AfterTransaction 
void afterTransaction() {
    // logic to be executed after a transaction has ended
}

@Sql

@Sql用於註解測試類或測試方法,以配置在集成測試期間針對給定數據庫運行的SQL腳本, 可以在之前或者之後運行, 配置好這個註解相應的屬性即可。示例:

@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) 
public void userTest() {
    // execute code that relies on the test schema and test data
}

@SqlConfig

@SqlConfig定義元數據,該元數據用於確定如何解析和運行使用@Sql註釋配置的SQL腳本, 用於指定公共前綴, 分割符等配置。示例:

@Test
@Sql(
    scripts = "/test-user-data.sql",
    config = @SqlConfig(commentPrefix = "`", separator = "@@") 
)
public void userTest {
    // execute code that relies on the test data
}

@SqlGroup

組合多個@Sql註解:

@Test
@SqlGroup({ 
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
public void userTest {
    // execute code that uses the test schema and test data
}

Spring的標準註解支持

支持以下註解:

@Autowired
@Qualifier
@Resource (javax.annotation) 如果存在JSR-250 的實現
@ManagedBean (javax.annotation) 如果存在 JSR-250 的實現
@Inject (javax.inject)
@Named (javax.inject)
@PersistenceContext (javax.persistence) 如果存在JPA 的實現
@PersistenceUnit (javax.persistence) 如果存在JPA 的實現
@Required
@Transactional

使用JUnit時支持的註解

@IfProfileValue

@IfProfileValue表示已爲特定的測試環境啓用帶註解的測試。如果配置的ProfileValueSource返回提供的名稱的匹配值,則啓用測試, 否則,禁用測試。

可以在類級別,方法級別或兩者上應用@IfProfileValue。對於該類或其子類中的任何方法,@ IfProfileValue的類級別用法優先於方法級別用法。

示例:

@IfProfileValue(name="java.vendor", value="Oracle Corporation") 
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}

可以使用值列表(帶有OR語義)配置@IfProfileValue:

@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) 
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
    // some logic that should run only for unit and integration test groups
}

@ProfileValueSourceConfiguration

@ProfileValueSourceConfiguration是一個類級別的註解,它指定@IfProfileValue註解使用哪種ProfileValueSource類型來檢索profile。如果未爲測試聲明@ProfileValueSourceConfiguration,則默認使用SystemProfileValueSource。以下示例顯示瞭如何使用@ProfileValueSourceConfiguration:

@ProfileValueSourceConfiguration(CustomProfileValueSource.class) 
public class CustomProfileValueSourceTests {
    // class body...
}

@Timed

@Timed表示帶註釋的測試方法必須在指定的時間段(以毫秒爲單位)內完成執行。如果文本執行時間超過指定的時間段,則測試將失敗。

該時間段包括運行測試方法本身,測試的任何重複(請參閱@Repeat)以及測試的任何setUp和tearDown的時間。示例:

@Timed(millis = 1000) 
public void testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to execute
}

Spring的@Timed註釋與JUnit 4的@Test(timeout = …)支持具有不同的語義。如果測試花費的時間太長,@ Test(timeout = …)會在測試完成之前失敗。另一方面,Spring的@Timed則是等待測試完成然後再失敗。

@Repeat

@Repeat表示帶註解的測試方法必須重複運行, 註解的value指定了重複的次數。
重複執行的範圍包括測試方法本身的執行以及測試的任何setUp或tearDown, 示例:

@Repeat(10) 
@Test
public void testProcessRepeatedly() {
    // ...
}

Spring TestContext框架

pring TestContext Framework(位於org.springframework.test.context包中)提供了通用的,註解驅動的單元和集成測試支持,這些支持與所使用的測試框架無關。 TestContext框架還非常重視約定優於配置,可以通過基於註解的配置覆蓋合理的默認值。

關鍵抽象

該框架的核心由TestContextManager類和TestContext,TestExecutionListener和SmartContextLoader接口組成。
爲每個測試類創建一個TestContextManager(例如,用於在JUnit Jupiter中的單個測試類中執行所有測試方法)。TestContextManager管理一個TestContext,它保存當前測試的上下文。隨着測試的進行,TestContextManager還更新了TestContext的狀態,並且回調通知TestExecutionListener的實現,這些TestExecutionListener提供依賴項注入,管理事務等功能。 SmartContextLoader負責爲給定的測試類加載ApplicationContext。

TestContext

TestContext封裝了在其中執行測試的上下文(與使用中的實際測試框架無關),併爲其負責的測試實例提供了上下文管理和緩存支持。如果被請求,TestContext還委託給SmartContextLoader來加載ApplicationContext。

TestContextManager

TestContextManager是Spring TestContext Framework的主要入口點,負責管理單個TestContext, 並在測試執行點向每個註冊的TestExecutionListener發出事件信號:

  • 在特定測試框架的任何“before class”或“before all”方法之前。
  • 測試實例後處理。
  • 在特定測試框架的任何“before”或“before each”方法之前。
  • 在執行測試方法之前但在setup方法之後。
  • 在執行測試方法之後但在tearDown方法之後。
  • 在特定測試框架的任何“after”或“after each”方法之後。
  • 在特定測試框架的任何“after class”或“after all”方法之後。

ContextLoader

ContextLoader是一個策略接口,用於爲Spring TestContext Framework管理的集成測試加載ApplicationContext。應該實現SmartContextLoader而不是此接口,以提供對組件類,活動bean定義配置文件,測試屬性源,上下文層次結構和WebApplicationContext支持的支持。

SmartContextLoader是Spring 3.1中引入的ContextLoader接口的擴展,取代了原始的最小ContextLoader SPI。具體來說,SmartContextLoader可以選擇處理資源位置,組件類或上下文初始化程序。此外,SmartContextLoader可以在其加載的上下文中bean配置文件和測試屬性源。

啓動TestContext框架

Spring TestContext Framework內部的默認配置足以滿足所有常見使用場景。但是,有時開發團隊或第三方框架希望更改默認的ContextLoader,實現自定義TestContext或ContextCache,擴展ContextCustomizerFactory和TestExecutionListener實現的默認集 ,等等。爲了對TestContext框架的運行方式進行低級控制,Spring提供了啓動策略。

TestContextBootstrapper定義用於引導TestContext框架的SPI。TestContextBootstrapper被TestContextManager用來加載TestExecutionListener實現, 和構建TestContext。可以@BootstrapWith直接使用或作爲元註釋,爲測試類(或測試類層次結構)配置自定義引導策略。如果啓動程序沒有明確地通過使用被配置@BootstrapWith,則DefaultTestContextBootstrapper或WebTestContextBootstrapper將被啓用。
由於TestContextBootstrapperSPI將來可能會發生變化(以適應新的要求),因此我們強烈建議實現者不要直接實現此接口,而應繼承AbstractTestContextBootstrapper或其某個具體子類。

TestExecutionListener配置

TestContext提供了默認就已經配置的TestExecutionListener:

  • ServletTestExecutionListener:配置Servlet API的mock WebApplicationContext。
  • DirtiesContextBeforeModesTestExecutionListener:處理的@DirtiesContext 模式爲“befroe”的註解。
  • DependencyInjectionTestExecutionListener:爲測試提供依賴注入。
  • DirtiesContextTestExecutionListener:處理的@DirtiesContext“after” 模式的註解。
  • TransactionalTestExecutionListener:提供事務能力, 默認回滾
  • SqlScriptsTestExecutionListener:運行@Sql 註解配置的SQL腳本。

合併默認的TestExecutionListener

如果TestExecutionListener通過來註冊自定義@TestExecutionListeners,則不會註冊默認的偵聽器。在很多場景下, 這迫使開發人員手動聲明除任何自定義偵聽器之外的所有默認偵聽器:

@ContextConfiguration
@TestExecutionListeners({
    MyCustomTestExecutionListener.class,
    ServletTestExecutionListener.class,
    DirtiesContextBeforeModesTestExecutionListener.class,
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    SqlScriptsTestExecutionListener.class
})
public class MyTest {
    // class body...
}

可以通過以下屬性啓用默認的監聽器:

@ContextConfiguration
@TestExecutionListeners(
    listeners = MyCustomTestExecutionListener.class,
    mergeMode = MERGE_WITH_DEFAULTS
)
public class MyTest {
    // class body...
}

上下文管理

TestContext爲其負責的每一個測試用例提供上下文管理和緩存支持。測試用例不會自動接收對已配置的訪問ApplicationContext。但是,如果測試類可以通過實現ApplicationContextAware接口,來獲得ApplicationContext引用。

使用@ContextConfiguration來配置上下文。
路徑可以是類路徑或者文件系統路徑:

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from "/app-config.xml" and
// "/test-config.xml" in the root of the classpath
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) 
public class MyTest {
    // class body...
}

如果省略註解中的位置屬性,則TestContext框架將嘗試檢測默認的XML資源位置。具體來說,GenericXmlContextLoader和GenericXmlWebContextLoader根據測試類的名稱檢測默認位置。如果您的類是命名的com.example.MyTest,則GenericXmlContextLoader從中加載應用程序上下文"classpath:com/example/MyTest-context.xml"。示例:

package com.example;

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration 
public class MyTest {
    // class body...
}

使用java配置類進行配置
示例:

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) 
public class MyTest {
    // class body...
}

對測試用例進行依賴注入

測試請求和會話作用域的bean

事務管理

執行Sql腳本

在針對關係數據庫編寫集成測試時,執行SQL腳本來修改數據庫模式或將測試數據插入表中通常是非常有意義的。

編程方式執行sql腳本

Spring提供了以下選項,用於在集成測試方法中以編程方式執行SQL腳本。

  • org.springframework.jdbc.datasource.init.ScriptUtils
  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests

ScriptUtils如果要對腳本如何解析和執行有完全的控制, 這個是更加合適的選擇。

ResourceDatabasePopulator提供用於配置在解析和運行腳本時使用的字符編碼,語句分隔符,註釋定界符和錯誤處理標誌的選項。它的內部也是ScriptUtils:

@Test
public void databaseTest {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScripts(
            new ClassPathResource("test-schema.sql"),
            new ClassPathResource("test-data.sql"));
    populator.setSeparator("@@");
    populator.execute(this.dataSource);
    // execute code that uses the test schema and data
}

使用@Sql聲明式執行SQL腳本

可以@Sql在測試類或測試方法上聲明註釋,以配置應該在集成測試方法之前或之後運行SQL腳本。方法級別的聲明將覆蓋類級別的聲明。對的支持@Sql由SqlScriptsTestExecutionListener提供,默認情況下啓用。
路徑資源語義
每個路徑都被解釋爲一個Spring Resource。純路徑(例如 “schema.sql”)被視爲相對於定義測試類的程序包的類路徑資源。以斜槓開頭的路徑被視爲絕對類路徑資源(例如"/org/example/schema.sql")。引用的URL的路徑(例如,前綴的路徑classpath:,file:,http:),通過使用指定的資源協議加載。
@Sql在基於JUnit Jupiter的集成測試:

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

    @Test
    void emptySchemaTest {
        // execute code that uses the test schema without any test data
    }
    
    @Test
    @Sql({"/test-schema.sql", "/test-user-data.sql"})
    void userTest {
        // execute code that uses the test schema and test data
    }
}

默認腳本檢測

如果未指定SQL腳本,則嘗試default根據@Sql聲明的位置來檢測腳本。如果無法檢測到默認值,則拋出一個IllegalStateException異常。

  • 類級別的聲明:如果帶註釋的測試類爲com.example.MyTest,則相應的默認腳本爲classpath:com/example/MyTest.sql。
  • 方法級別的聲明:如果已命名testMethod()並在類中定義了帶註釋的測試方法com.example.MyTest,則相應的默認腳本爲classpath:com/example/MyTest.testMethod.sql。

聲明多個@Sql

如果需要爲給定的測試類或測試方法配置多組SQL腳本,而且使用不同的語法配置,不同的錯誤處理規則或每組不同的執行階段,則可以聲明的多個實例@Sql。使用Java 8,可以將@Sql用作可重複zhujie。否則,需要用 @SqlGroup聲明的多個 @Sql。
以下示例說明如何在@SqlJava 8中用作可重複註解:

@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
public void userTest {
    // execute code that uses the test schema and test data
}

兼容java8以下的語法:

@Test
@SqlGroup({
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
public void userTest {
    // execute code that uses the test schema and test data
}

腳本執行階段

默認情況sql腳本是在測試用例運行之前執行的。 但是也可以通過配置在執行測試之後運行一段腳本。示例:

@Test
@Sql(
    scripts = "create-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
    scripts = "delete-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED),
    executionPhase = AFTER_TEST_METHOD
)
public void userTest {
    // execute code that needs the test data to be committed
    // to the database outside of the test's transaction
}

配置 @SqlConfig

可以使用@SqlConfig註解配置腳本解析和錯誤處理。當在集成測試類上聲明爲類級別的註解時,@SqlConfig 充當測試類層次結構中所有SQL腳本的全局配置。通過使用@Sql註解的config屬性直接聲明時,@SqlConfig 用作這個@Sql 註解中聲明的SQL腳本的配置。@SqlConfig中的每個屬性都有一個隱式默認值,由於Java語言規範中爲註釋屬性定義了規則,因此無法分配null註釋屬性。因此,爲了支持對繼承的全局配置的覆蓋,@SqlConfig屬性具有""(對於字符串)或DEFAULT(對於枚舉)的顯式默認值。通過提供非默認值,可以使局部@SqlConfig覆蓋全局聲明中的各個屬性。只要局部 屬性不提供默認值之外的任何顯式制定,就會繼承全局屬性。

並行執行測試

Spring 5.0支持在一個JVM中並行執行多個測試。
以下情況不適合使用並行執行測試:

  • 使用@DirtiesContext時;
  • 使用JUnit的@FixMethodOrder或者其他框架的類似配置時;
  • 改變外部系統狀態時, 如更改數據庫, 消息中間件, 文件系統等;

總之, 是並行和串行語意不等同時。

框架支持類

Spring JUnit 4 Runner

可以指定Spring 的Runner在Junit4在獲得TestContext等支持:
示例:

@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {

    @Test
    public void testMethod() {
        // execute test logic...
    }
}

在前面的示例中,爲@TestExecutionListeners配置了一個空列表以禁用默認偵聽器,否則將需要通過@ContextConfiguration配置ApplicationContext。

Spring Junit 4 Rules

SpringClassRule是一個JUnit TestRule,它支持Spring TestContext Framework的類級功能,而SpringMethodRule是一個JUnit MethodRule,它支持Spring TestContext Framework的實例級和方法級功能。

與SpringRunner相比,Spring的基於規則的JUnit支持具有獨立於任何org.junit.runner.Runner實現的優點,因此可以與現有的替代Runner(例如JUnit 4的Parameterized)或第三方Runner結合使用(例如MockitoJUnitRunner)。

爲了支持TestContext框架的全部功能,必須將SpringClassRule與SpringMethodRule結合使用:

// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {

    @ClassRule
    public static final SpringClassRule springClassRule = new SpringClassRule();
    
    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();
    
    @Test
    public void testMethod() {
        // execute test logic...
    }
}

Junit 4 支持類

AbstractJUnit4SpringContextTests是抽象的基礎測試類,該類可以獲取ApplicationContext實例。

AbstractTransactionalJUnit4SpringContextTests是AbstractJUnit4SpringContextTests的針對事務的抽象子類,爲JDBC訪問添加了一些便利功能。

關於Junit Jupiter的Spring擴展

Spring TestContext Framework提供了與JUnit 5中引入的JUnit Jupiter測試框架的完全集成。通過使用@ExtendWith(SpringExtension.class)註釋測試類,可以引入TestContext框架。

Spring在Spring支持JUnit 4和TestNG的功能集之外提供了以下功能:

  • 測試構造函數,測試方法和測試生命週期回調方法的依賴注入。
  • 基於SpEL表達式,環境變量,系統屬性等的條件測試執行的強大支持。
  • 定製組合註解,結合了Spring和JUnit Jupiter的註釋。

以下示例展示如何配置測試類以將SpringExtension與@ContextConfiguration結合使用:

// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {

    @Test
    void testMethod() {
        // execute test logic...
    }
}

由於還可以在JUnit 5中將註解用作元註解,因此Spring提供了@SpringJUnitConfig和@SpringJUnitWebConfig,以簡化測試ApplicationContext和JUnit Jupiter的配置:

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {

    @Test
    void testMethod() {
        // execute test logic...
    }
}

WebApplicationContext 示例:

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {

    @Test
    void testMethod() {
        // execute test logic...
    }
}

TestNG支持類

  • AbstractTestNGSpringContextTests是一個抽象的基礎測試類,可以獲取ApplicationContext的引用。
  • AbstractTransactionalTestNGSpringContextTests是AbstractTestNGSpringContextTests的抽象事務擴展,爲JDBC訪問添加了一些便利功能。

Spring MVC 測試框架

可以在不運行servlet服務器時, 提供對mvc的環境, 對mvc應用進行測試。

服務端測試

Spring MVC Test的目標是通過執行請求並通過實際的DispatcherServlet生成響應來提供一種測試控制器的有效方法。
Spring MVC Test基於spring-test模塊中可用的Servlet API的“模擬”實現。這允許執行請求和生成響應,而無需在Servlet容器中運行。在大多數情況下,一切都應像在運行時一樣工作。以下是基於JUnit Jupiter的示例:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class ExampleTests {

    private MockMvc mockMvc;
    
    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    
    @Test
    void getAccount() throws Exception {
        this.mockMvc.perform(get("/accounts/1")
                .accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json"))
            .andExpect(jsonPath("$.name").value("Lee"));
    }
}

設置選擇

有兩種方式來創建MockMvc實例。
首先是通過TestContext框架加載Spring MVC配置,該框架加載Spring配置並將WebApplicationContext注入測試中以用於構建MockMvc實例。以下示例顯示瞭如何執行此操作:

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("my-servlet-context.xml")
public class MyWebTests {

    @Autowired
    private WebApplicationContext wac;
    
    private MockMvc mockMvc;
    
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }
    
    // ...

}

第二個選擇是在不加載Spring配置的情況下手動創建控制器實例。可以在一定程度上對其進行自定義。以下示例顯示瞭如何執行此操作:

public class MyWebTests {

    private MockMvc mockMvc;
    
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
    }
    
    // ...

}

webAppContextSetup加載實際的Spring MVC配置,從而進行更完整的集成測試。由於TestContext框架緩存了已加載的Spring配置,因此即使在測試套件中引入了更多測試,它也有助於保持測試的快速運行。此外,可以通過Spring配置將模擬服務注入控制器中,以專注於測試Web層。下面的示例使用Mockito聲明一個模擬服務:

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="org.example.AccountService"/>
</bean>

測試示例:

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {

    @Autowired
    private WebApplicationContext wac;
    
    private MockMvc mockMvc;
    
    @Autowired
    private AccountService accountService;
    
    // ...

}

另一方面,standaloneSetup更接近於單元測試, 它一次測試一個控制器。

與大多數“集成與單元測試”爭辯一樣,沒有正確或錯誤的答案。

設置mvc特性

無論使用哪種MockMvc構建器,所有MockMvcBuilder實現都提供一些常見且非常有用的功能, 配置一切全局的except:

// static import of MockMvcBuilders.standaloneSetup
MockMvc mockMvc = standaloneSetup(new MusicController())
        .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
        .alwaysExpect(status().isOk())
        .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
        .build();

設置session:

// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
        .apply(sharedHttpSession())
        .build();

// Use mockMvc to perform requests...

參見ConfiurableMockMvcBuilder的Javadoc.

執行請求

示例:

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

執行模擬的Multipart請求:

mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

可以使用URI模板風格指定查詢參數:

mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));

可以添加代表查詢或表單參數的Servlet請求參數,如以下示例所示:

mockMvc.perform(get("/hotels").param("thing", "somewhere"));

注意,URI模板提供的查詢參數會被解碼,而通過param(…)方法提供的請求參數則被當成已被解碼。

設置contextPath和servletPath:

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

設置全局默認的contextPath和servletPath:

public class MyWebTests {

    private MockMvc mockMvc;
    
    @Before
    public void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}

定義期望

可以通過在執行請求後追加一個或多個.andExpect(…)調用來定義期望:

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

期望分爲兩大類。
第一類斷言驗證響應的屬性(例如,響應狀態,標頭和內容)。這些是要斷言的重要的內容。

第二類斷言超出了響應範圍。這些斷言使您可以檢查Spring MVC的特定方面,例如哪種控制器方法處理了請求,是否引發和處理了異常,模型的內容是什麼,選擇了哪種視圖,添加了哪些閃存屬性,等等。它們還使您可以檢查Servlet的特定方面,例如請求和會話屬性。

以下測試斷言綁定或驗證失敗:

mockMvc.perform(post("/persons"))
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));

在某些情況下,可能需要直接訪問結果並驗證否則無法驗證的內容。可以通過在所有其他期望之後附加.andReturn()來實現,如以下示例所示:

MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...

如果所有測試都重複相同的期望,則在構建MockMvc實例時可以一次設置通用期望,如以下示例所示:

standaloneSetup(new SimpleController())
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build()

當響應內容是JSON時,可以使用JsonPath表達式來驗證結果,如以下示例所示:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

當響應內容是XML時:

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
    .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

註冊filter

設置MockMvc實例時,可以註冊一個或多個Servlet Filter實例:

mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

Spring MVC 測試 vs 端到端測試

對於Spring mvc測試,默認情況下沒有上下文路徑。沒有jsessionid cookie;沒有轉發,錯誤或異步調度;因此,沒有實際的JSP呈現。而是將“轉發”和“重定向” URL保存在MockHttpServletResponse中,並且可以按預期進行聲明。

可以考慮使用@WebIntegrationTest從Spring Boot獲得完整的端到端集成測試支持。

測試示例

https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/servlet/samples
https://github.com/spring-projects/spring-mvc-showcase/tree/master/src

HtmlUnit集成

Spring提供了MockMvc和HtmlUnit之間的集成。使用基於HTML的視圖時,這簡化了執行端到端測試的過程。 具有如下的能力:

  • 使用HtmlUnit,WebDriver和Geb等工具可以輕鬆測試HTML頁面,而無需部署到Servlet容器。
  • 測試頁面內的JavaScript。
  • 使用模擬服務進行測試以加快測試速度。
  • 在容器內端到端測試和容器外集成測試之間共享邏輯。

爲什麼集成 HtmlUnit

通過一個示例來了解, 這種集成的必要性.
假設有一個Spring MVC Web應用程序,該應用程序支持對Message對象的CRUD操作。該應用程序還支持所有消息的分頁。將如何進行測試?

使用Spring MVC Test,我們可以輕鬆地測試是否能夠創建Message,如下所示:

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param("summary", "Spring Rocks")
        .param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

如果我們要測試創建消息的表單視圖怎麼辦?例如,假設我們的表單類似於以下代碼段:

<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a></div>

    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />
    
    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>
    
    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>

我們如何確保表單產生正確的請求以創建新消息?天真的嘗試可能類似於以下內容:

mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='summary']").exists())
        .andExpect(xpath("//textarea[@name='text']").exists());

此測試有一些明顯的缺點。如果我們更新控制器以使用參數message而不是text,則即使HTML表單與控制器不同步,我們的表單測試也會繼續通過。爲了解決這個問題,我們可以結合以下兩個測試:

String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
        .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param(summaryParamName, "Spring Rocks")
        .param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

這樣可以減少測試不正確通過的風險,但是仍然存在一些問題:

  • 如果頁面上有多個表單怎麼辦?誠然,我們可以更新XPath表達式,但是由於我們考慮了更多因素,它們變得更加複雜:字段是正確的類型嗎?是否啓用了字段?等等

  • 另一個問題是我們正在做我們期望的兩倍的工作。我們必須首先驗證視圖,然後使用剛剛驗證的相同參數提交視圖。理想情況下,可以一次完成所有操作。

  • 最後,我們仍然無法解釋某些事情。例如,如果表單也具有我們希望測試的JavaScript驗證,該怎麼辦?

總體問題是,測試網頁不涉及單個交互。相反,它是用戶如何與網頁交互以及該網頁與其他資源交互的組合。例如,表單視圖的結果用作用戶創建消息的輸入。另外,我們的表單視圖可以潛在地使用影響頁面行爲的其他資源,例如JavaScript驗證。

端到端測試

爲了解決前面提到的問題,我們可以執行端到端集成測試:

  • 我們的頁面是否向用戶顯示通知,以指示消息爲空時沒有可用結果?
  • 我們的頁面是否正確顯示一條消息?
  • 我們的頁面是否正確支持分頁?

要設置這些測試,我們需要確保我們的數據庫包含正確的消息。這帶來了許多其他挑戰:

  • 確保數據庫中包含正確的消息可能很繁瑣。 (考慮外鍵約束。)
  • 測試可能會變慢,因爲每次測試都需要確保數據庫處於正確的狀態。
  • 由於我們的數據庫需要處於特定狀態,因此我們無法並行運行測試。
  • 對諸如自動生成的ID,時間戳等項目進行斷言可能很困難。

這些挑戰並不意味着應該完全放棄端到端集成測試。相反,我們可以通過重構我們的詳細測試以使用運行速度更快,更可靠且沒有副作用的模擬服務來減少端到端集成測試的數量。然後,我們可以實施少量真正的端到端集成測試,以驗證簡單的工作流程,以確保一切正常工作。

那麼,如何在測試頁面的交互性之間保持平衡,並在測試套件中保持良好的性能呢?答案是:“通過將MockMvc與HtmlUnit集成。”

集成HtmlUnit的選擇

要將MockMvc與HtmlUnit集成時,有很多選擇:

  • MockMvc和HtmlUnit:如果要使用原始的HtmlUnit庫,請使用此選項。
  • MockMvc和WebDriver:使用此選項可簡化集成和端到端測試之間的開發和重用代碼。
  • MockMvc和Geb:如果要使用Groovy進行測試,簡化開發並在集成和端到端測試之間重用代碼,請使用此選項。

MockMvc和HtmlUnit

配置

首先,請確保已包含對net.sourceforge.htmlunit:htmlunit的測試依賴項。爲了將HtmlUnit與Apache HttpComponents 4.5+一起使用,需要使用HtmlUnit 2.18或更高版本。

可以使用MockMvcWebClientBuilder輕鬆創建一個與MockMvc集成的HtmlUnit WebClient,如下所示:

@Autowired
WebApplicationContext context;

WebClient webClient;

@Before
public void setup() {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

這樣可以確保將引用localhost作爲服務器的所有URL定向到我們的MockMvc實例,而無需真正的HTTP連接。通常,通過使用網絡連接來請求其他任何URL。這使我們可以輕鬆測試CDN的使用。

使用

現在,我們可以像往常一樣使用HtmlUnit,而無需將應用程序部署到Servlet容器。例如,我們可以請求視圖創建以下消息:

HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

一旦有了對HtmlPage的引用,就可以填寫該表單並提交以創建一條消息,如以下示例所示:

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

最後,我們可以驗證是否成功創建了新消息。以下斷言使用AssertJ庫:

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

前面的代碼以多種方式改進了我們的MockMvc測試。首先,我們不再需要顯式驗證表單,然後創建類似於表單的請求。相反,我們請求表單,將其填寫並提交。
另一個重要因素是HtmlUnit使用Mozilla Rhino引擎來評估JavaScript。這意味着還可以在頁面內測試JavaScript的行爲。

MockMvcWebClientBuilder高級特性

配置更加高級的東西:

@Before
public void setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
}

或者,我們可以通過分別配置MockMvc實例並將其提供給MockMvcWebClientBuilder來執行完全相同的設置,如下所示:

MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();

MockMvc 和 WebDriver

在Selenium WebDriver中使用其他抽象簡化測試的編寫。

爲什麼使用 MockMvc 和 WebDriver?

Selenium WebDriver提供了一個非常優雅的API,使我們可以輕鬆地組織代碼。爲了更好地說明其工作原理,我們在本節中探索一個示例。

假設我們需要確保正確創建一條消息。測試涉及找到HTML表單輸入元素,將其填寫並做出各種斷言。

這種方法會導致大量單獨的測試,因爲我們也想測試錯誤情況。例如,如果只填寫表格的一部分,我們要確保得到一個錯誤。

如果其中一個字段被命名爲“ summary”,我們可能會在測試中的多個位置重複以下內容:

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

那麼,如果我們將ID更改爲smmry,會發生什麼?這樣做將迫使我們更新所有測試以納入此更改。這違反了DRY原則,因此理想情況下,我們應將此代碼提取到其自己的方法中,如下所示:

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}

這樣做可以確保在更改UI時不必更新所有測試用例。 甚至可以更進一步,將此邏輯放在代表我們當前所在的HtmlPage的Object中,如以下示例所示:


public class CreateMessagePage {

    final HtmlPage currentPage;
    
    final HtmlTextInput summaryInput;
    
    final HtmlSubmitInput submit;
    
    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }
    
    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);
    
        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);
    
        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }
    
    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }
    
    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}

此模式稱爲頁面對象模式。雖然我們當然可以使用HtmlUnit做到這一點,但WebDriver提供了一些我們在以下各節中探討的工具,以使該模式的實現更加容易。

MockMvc和WebDriver設置

要將Selenium WebDriver與Spring MVC Test框架一起使用,請確保項目包含對org.seleniumhq.selenium:selenium-htmlunit-driver的測試依賴項。

我們可以使用MockMvcHtmlUnitDriverBuilder輕鬆創建一個與MockMvc集成的Selenium WebDriver:

 @Autowired
    WebApplicationContext context;

    WebDriver driver;
    
    @Before
    public void setup() {
        driver = MockMvcHtmlUnitDriverBuilder
                .webAppContextSetup(context)
                .build();
}

前面的示例確保將引用localhost作爲服務器的所有URL定向到我們的MockMvc實例,而無需真正的HTTP連接。通常,通過使用網絡連接來請求其他任何URL。這使我們可以輕鬆測試CDN的使用。

用法

現在,我們可以像往常一樣使用WebDriver,而無需將應用程序部署到Servlet容器。例如,我們可以請求視圖:

CreateMessagePage page = CreateMessagePage.to(driver);

填寫表格並提交以創建一條消息,如下所示:

ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

通過利用Page Object Pattern,這可以改善我們的HtmlUnit測試的設計。正如我們在“爲什麼要使用WebDriver和MockMvc?”中提到的那樣,我們可以將頁面對象模式與HtmlUnit一起使用,但使用WebDriver則要容易得多。考慮以下CreateMessagePage實現:

public class CreateMessagePage
        extends AbstractPage { 


    private WebElement summary;
    private WebElement text;


​    
​    @FindBy(css = "input[type=submit]")private WebElement submit;public CreateMessagePage(WebDriver driver) {
        super(driver);
    }
    
    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }
    
    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}

斷言結果:

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

可以看到ViewMessagePage允許我們與自定義域模型進行交互。例如,它公開了一個返回Message對象的方法:

public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}

測試完畢之後關閉WebDriver示例:

@After
public void destroy() {
    if (driver != null) {
        driver.close();
    }
}
MockMvcHtmlUnitDriverBuilder 的高級用法

配置全局屬性:

   WebDriver driver;

    @Before
    public void setup() {
        driver = MockMvcHtmlUnitDriverBuilder
                // demonstrates applying a MockMvcConfigurer (Spring Security)
                .webAppContextSetup(context, springSecurity())
                // for illustration only - defaults to ""
                .contextPath("")
                // By default MockMvc is used for localhost only;
                // the following will use MockMvc for example.com and example.org as well
                .useMockMvcForHosts("example.com","example.org")
                .build();
}

客戶端REST測試

這裏的客戶端測試指測試使用restTemplate調用遠程服務的代碼。 基本思想是mock這些調用, 讓我們不需要啓動相應的遠程服務器:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

// Test code that uses the above RestTemplate ...

mockServer.verify();

默認情況下,請求應按聲明的期望順序進行。可以在構建服務器時設置ignoreExpectOrder選項,在這種情況下,將檢查所有期望值(以便)以找到給定請求的匹配項。這意味着允許請求以任何順序出現。以下示例使用ignoreExpectOrder:

server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

默認情況下即使是無順序請求,每個請求也只能執行一次。 Expect方法提供了一個重載的變量,該變量接受一個ExpectedCount參數,該參數指定一個計數範圍(例如,一次,多次,最大,最小,之間等等)。以下示例使用時間:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();

可以將RestTemplate以將其綁定到MockMvc實例。這樣就可以使用實際的服務器端邏輯來處理請求,而無需運行服務器。以下示例顯示瞭如何執行此操作:

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...


WebTestClient(用於WebFlux應用的測試)

WebTestClient是WebClient的薄封裝,可用於執行請求並公開專用的流暢風格API來驗證響應。 WebTestClient通過使用模擬請求和響應綁定到WebFlux應用程序,或者它可以通過HTTP連接測試任何Web服務器。

配置

綁定到Controller上

綁定到一個@Controller:

client = WebTestClient.bindToController(new TestController()).build();

綁定到Router 函數

示例:

    RouterFunction<?> route = ...
    client = WebTestClient.bindToRouterFunction(route).build();

在內部,配置被傳遞到RouterFunctions.toWebHandler。通過使用模擬請求和響應對象,可以在沒有HTTP服務器的情況下測試WebFlux應用程序。

綁定到ApplicationContext中

示例:

    @RunWith(SpringRunner.class)
    @ContextConfiguration(classes = WebConfig.class) 
    public class MyTests {

        @Autowired
        private ApplicationContext context; 
    
        private WebTestClient client;
    
        @Before
        public void setUp() {
            client = WebTestClient.bindToApplicationContext(context).build(); 
        }
    }

綁定到一個正在運行的服務器

示例:

client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();

使用Client Builder配置一些屬性

示例:

client = WebTestClient.bindToController(new TestController())
            .configureClient()
            .baseUrl("/test")
            .build();

用法

WebTestClient提供了與WebClient相同的API,直到使用exchange()執行請求爲止。 exchange()之後是鏈接的API工作流,用於驗證響應。
通常,首先聲明響應狀態和標頭,如下所示:

   client.get().uri("/persons/1")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .exchange()
            .expectStatus().isOk()
            .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
            // ...

接下來, 可以指定如何解碼和如何處理響應的body:

  • ExpectBody(Class<T>):解碼爲單個對象。
  • ExpectBodyList(Class<T>):解碼並將對象收集到List<T>
  • ExpectBody():解碼爲byte []以獲取JSON內容或一個空的正文。

然後,可以對主體使用內置的斷言。下面的示例顯示了一種方法:

   client.get().uri("/persons")
            .exchange()
            .expectStatus().isOk()
            .expectBodyList(Person.class).hasSize(3).contains(person);

可以自定義自己的斷言:

 client.get().uri("/persons/1")
            .exchange()
            .expectStatus().isOk()
            .expectBody(Person.class)
            .consumeWith(result -> {
                // custom assertions (e.g. AssertJ)...
            });

退出流, 直接獲取一個結果:

 EntityExchangeResult<Person> result = client.get().uri("/persons/1")
            .exchange()
            .expectStatus().isOk()
            .expectBody(Person.class)
            .returnResult();

No Content

如果響應沒有內容(或者內容不關緊要),請使用Void.class,以確保釋放資源:

 client.get().uri("/persons/123")
            .exchange()
            .expectStatus().isNotFound()
            .expectBody(Void.class);

或者,如果要斷言沒有響應內容,則可以使用類似於以下內容的代碼:

client.post().uri("/persons")
            .body(personMono, Person.class)
            .exchange()
            .expectStatus().isCreated()
            .expectBody().isEmpty();

JSON Content

對響應體進行JSON相關的斷言.
全匹配:

  client.get().uri("/persons/1")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .json("{\"name\":\"Jane\"}")

使用jsonPath:


   client.get().uri("/persons")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$[0].name").isEqualTo("Jane")
            .jsonPath("$[1].name").isEqualTo("Jason");

流響應

要測試無限流(例如,“ text / event-stream”或“ application / stream + json”),需要在響應狀態和header斷言之後立即退出鏈接的API(使用returnResult):

FluxExchangeResult<MyEvent> result = client.get().uri("/events")
            .accept(TEXT_EVENT_STREAM)
            .exchange()
            .expectStatus().isOk()
            .returnResult(MyEvent.class);

校檢結果:

  Flux<Event> eventFux = result.getResponseBody();

    StepVerifier.create(eventFlux)
            .expectNext(person)
            .expectNextCount(4)
            .consumeNextWith(p -> ...)
            .thenCancel()
            .verify();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章