’Vert.x導論‘回顧
現(xiàn)在讓我們快速回顧到目前為止在Vert.x導論系列中我們開發(fā)了些什么。在第一篇帖子中唉堪,我們開發(fā)了一個非常簡單的Vert.x 3應(yīng)用模聋,并且學習了這個應(yīng)用如何被測試,打包及執(zhí)行唠亚。在第二篇帖子中,我們學習了這個應(yīng)用如何可配置链方,并在測試中采用了隨機端口。最后灶搜,在上一篇帖子中祟蚀,展示了如何使用vertx-web以及如何實現(xiàn)一個小型的REST API。然而割卖,我們忘記了一個重要任務(wù)前酿。我們沒有測試新增的API。在這篇帖子中鹏溯,我們會通過實現(xiàn)單元測試以及集成測試來增加我們對新功能的信心罢维。
這篇帖子的代碼在Introduction-to-Vert.x-Demo項目的post-4分支。起始代碼在post-3分支剿涮。
測試言津,測試攻人,再測試取试。。怀吻。
這篇帖子主要關(guān)于測試瞬浓。我們區(qū)分兩種測試:單元測試和集成測試。兩者同等重要蓬坡,但是關(guān)注點不同猿棉。單元測試確保你應(yīng)用的一個組件正常工作磅叛,通常就是Java世界內(nèi)的一個class行為符合預(yù)期。應(yīng)用并沒有作為一個整體被測試萨赁,而是一部分一部分的測試弊琴。集成測試感覺更黑盒測試因為應(yīng)用通常從外部啟動和測試。
在這篇帖子中杖爽,我們將從更多的單元測試起步作為熱身敲董,然后聚焦于集成測試。如果你之前實現(xiàn)過集成測試慰安,你可能會被嚇到腋寨,這說得通。但不用怕化焕,用Vert.x開發(fā)沒有隱藏的驚嚇萄窜。
熱身:更多的單元測試
我們慢慢來。在第一篇帖子里撒桨,我們用vertx-unit實現(xiàn)了一個單元測試查刻。我們之前做的這個測試超級簡單:
- 我們在測試前啟動了應(yīng)用
- 我們檢驗它是否以"Hello"作為響應(yīng)
為了方便你回憶,讓我們看看這段代碼
@Before
public void setUp(TestContext context) throws IOException {
vertx = Vertx.vertx();
ServerSocket socket = new ServerSocket(0);
port = socket.getLocalPort();
socket.close();
DeploymentOptions options = new DeploymentOptions()
.setConfig(new JsonObject().put("http.port", port)
);
vertx.deployVerticle(MyFirstVerticle.class.getName(), options, context.asyncAssertSuccess());
}
setUp
方法在每次測試前都會被調(diào)用(@Before
注解指定這樣操作)凤类。這個方法首先創(chuàng)建一個Vert.x的新 實例赖阻,然后獲取一個可用端口,最后根據(jù)對應(yīng)的配置來部署我們的verticle踱蠢。context.asyncAssertSuccess()
方法會一直等待直到verticle被成功部署好為止火欧。
tearDown
方法是簡單明了的,只是關(guān)閉了Vert.x實例茎截。它自動卸載了verticles:
@After
public void tearDown(TestContext context) {
vertx.close(context.asyncAssertSuccess());
}
最終苇侵,我們的單個測試是:
@Test
public void testMyApplication(TestContext context) {
final Async async = context.async();
vertx.createHttpClient().getNow(port, "localhost", "/", response -> {
response.handler(body -> {
context.assertTrue(body.toString().contains("Hello"));
async.complete();
});
});
}
這個測試只是檢測當我們對"/"地址發(fā)送一個HTTP請求時,應(yīng)用是否回復(fù)了"Hello"∑笮浚現(xiàn)在我們嘗試實現(xiàn)一些單元測試來確認我們的web應(yīng)用和REST API接口的行為是否符合預(yù)期榆浓。我們首先檢查"index.html"頁面是否正確工作。這個測試和之前那個測試很相似撕攒。
@Test
public void checkThatTheIndexPageIsServed(TestContext context) {
Async async = context.async();
vertx.createHttpClient().getNow(port, "localhost", "/assets/index.html", response -> {
context.assertEquals(response.statusCode(), 200);
context.assertEquals(response.headers().get("Content-Type"), "text/html");
response.bodyHandler(body -> {
context.assertTrue(body.toString().contains("<title>My Whisky Collection</title>"));
async.complete();
});
});
}
我們檢索了index.html頁面并檢查:
- 頁面存在(狀態(tài)碼200)
- 這是個HTML頁面(Content-Type被設(shè)置為"text/html")
- 頁面的標題正確("My Whisky Collection")
檢索內(nèi)容
如你所見陡鹃,我們可以在HTTP響應(yīng)上直接測試狀態(tài)碼和消息頭,但我們需要檢索消息體來確保它是正確的抖坪。這通過接受整個消息體作為參數(shù)的消息體句柄來做到的萍鲸。一旦最后的檢驗完成,我們通過調(diào)用complete
來釋放async
擦俐。
很好脊阴,但這實際上并沒有測試我們的REST API。先確認我們可以在集合中增加一瓶葡萄酒。不像之前的測試嘿期,這個測試使用post
方法post數(shù)據(jù)到服務(wù)器:
@Test
public void checkThatWeCanAdd(TestContext context) {
Async async = context.async();
final String json = Json.encodePrettily(new Whisky("Jameson", "Ireland"));
final String length = Integer.toString(json.length());
vertx.createHttpClient().post(port, "localhost", "/api/whiskies")
.putHeader("content-type", "application/json")
.putHeader("content-length", length)
.handler(response -> {
context.assertEquals(response.statusCode(), 201);
context.assertTrue(response.headers().get("content-type").contains("application/json"));
response.bodyHandler(body -> {
final Whisky whisky = Json.decodeValue(body.toString(), Whisky.class);
context.assertEquals(whisky.getName(), "Jameson");
context.assertEquals(whisky.getOrigin(), "Ireland");
context.assertNotNull(whisky.getId());
async.complete();
});
})
.write(json)
.end();
}
首先我們創(chuàng)建我們想要添加的內(nèi)容品擎。服務(wù)器消費JSON數(shù)據(jù),所以我們需要一個JSON字符串备徐。你可以手工寫出你的JSON文檔萄传,或者和這里一樣使用Vert.x方法(Json.encodePrettily
)。一旦我們準備好了內(nèi)容蜜猾,我們做一個POST
請求盲再。我們需要配置一些消息頭來確保我們的JSON數(shù)據(jù)被服務(wù)器正確讀取。我們表示我們在發(fā)送JSON數(shù)據(jù)并且還設(shè)置了消息體的長度瓣铣。我們還附加了一個響應(yīng)句柄做了類似前面測試的檢測答朋。請注意我們可以使用JSON.decodeValue
方法將服務(wù)器發(fā)送的JSON文檔重構(gòu)成我們需要的對象。這樣做可以避免很多樣板代碼所以很方便棠笑。此刻梦碗,請求還沒有發(fā)送,我們需要寫出數(shù)據(jù)并調(diào)用end()方法蓖救。這通過 .write(json).end();
來辦到洪规。
方法的順序很重要。如果你沒有配置好響應(yīng)句柄循捺,你不能寫出數(shù)據(jù)斩例。最后不要忘記調(diào)用end()
。
你可以使用如下命令來執(zhí)行測試:
mvn clean test
我們可以寫更多類似這樣的單元測試从橘,但這將變得很復(fù)雜念赶。下面將使用集成測試來繼續(xù)我們的測試工作。
集成測試很傷人
我想我們首先需要明確恰力,集成測試很折磨人叉谜。如果你在這個領(lǐng)域有經(jīng)驗,你還記得要花多久讓一切事物就緒踩萎?一想起這事我就頭疼停局。為何集成測試越來越麻煩了?主要在于安裝環(huán)節(jié):
- 我們必須以近似生產(chǎn)環(huán)境的方式來啟動應(yīng)用
- 接下來要運行測試(配置測試確保檢查的是所需的應(yīng)用實例)
- 最后必須停止應(yīng)用
聽上去并不麻煩香府,但如果你需要Linux董栽,MacOS X和Windows的支持,事情很快變得凌亂起來企孩。有很多了不起的框架可以解決這個問題比如Arquillian锭碳,但這里我們將不使用框架做集成測試,以便更好的理解工作機理柠硕。
我們需要一份戰(zhàn)斗計劃
在投入復(fù)雜的配置前工禾,我們先花點時間確認下任務(wù):
第一步 - 保留一個可用端口 我們需要獲取一個應(yīng)用可以監(jiān)聽的可用端口运提,并且我們需要將這個端口注入到集成測試中蝗柔。
第二步 - 生成應(yīng)用配置 一旦準備好了可用端口闻葵,我們需要寫一個JSON文件配置這個端口為應(yīng)用的HTTP端口
第三步 - 啟動應(yīng)用 聽起來很容易?由于我們需要在后臺進程中啟動應(yīng)用癣丧,所以也并不那么簡單槽畔。
第四步 - 執(zhí)行集成測試 最后,重點部分胁编,運行測試厢钧。但在這之前,我們應(yīng)該事先一些集成測試嬉橙。我們后面將會提到早直。
第五步 - 停止應(yīng)用 一旦測試都執(zhí)行完成,無論測試中是否有失敗或錯誤市框,我們需要停止應(yīng)用霞扬。
有多種方式可以實現(xiàn)這份計劃。我們打算采用一種通用的方式杆查。這也許不是最好的页滚,但幾乎可以在任何場合使用泊愧。這種方法和Apache Maven綁的很緊。如果你想提議一種替代方案(采用Gradle或者其他工具)斧拍,我很高興能把你的方法添加到這篇帖子中。
實現(xiàn)這份計劃
如上所說杖小,這章節(jié)以Maven為中心肆汹,大部分代碼在pom.xml文件中。如果你從未使用過不同的Maven生命周期階段予权,推薦你讀一下introduction to the Maven lifecycle县踢。
我們需要添加和配置一些插件。打開pom.xml
文件伟件,在<plugins>
部分添加:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.9.1</version>
<executions>
<execution>
<id>reserve-network-port</id>
<goals>
<goal>reserve-network-port</goal>
</goals>
<phase>process-sources</phase>
<configuration>
<portNames>
<portName>http.port</portName>
</portNames>
</configuration>
</execution>
</executions>
</plugin>
我們使用build-helper-maven-plugin
(如果你經(jīng)常使用Maven你應(yīng)該去了解下)來獲取一個可用端口硼啤。一旦確定,這個插件將可用端口賦值給http.port
變量斧账。我們在構(gòu)建過程的早期執(zhí)行這個插件(在process-sources
階段)谴返,這樣我們可以在其他插件中使用http.port變量。這是為了第一步做準備咧织。
第二步需要執(zhí)行兩個動作嗓袱。首先,在pom.xml
文件中习绢,緊跟在<build>
開放標簽下渠抹,添加:
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
這里指示Maven從 src/test/resources
目錄過濾資源蝙昙。Filter意味著用真實值代替占位符。這正是我們所需的梧却,現(xiàn)在我們有了http.port
變量∑娴撸現(xiàn)在用如下內(nèi)容來創(chuàng)建 src/test/resources/my-it-config.json
文件:
{
"http.port": ${http.port}
}
這個配置文件類似于我們在之前帖子中創(chuàng)建的那個。唯一的差別在于${http.port}
放航,這也是Maven過濾用的默認語法烈拒。所以,當Maven需要處理文件時广鳍,它將會用被選的端口來替換${http.port}
荆几。這就是第二步。
第三步和第五步的處理比較麻煩赊时。我們要啟動和停止應(yīng)用吨铸。我們打算用maven-antrun-plugin來辦到。在pom.xml文件中祖秒,在build-helper-maven-plugin下诞吱,添加:
<!-- We use the maven-antrun-plugin to start the application before the integration tests
and stop them afterward -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>start-vertx-app</id>
<phase>pre-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<!--
Launch the application as in 'production' using the fatjar.
We pass the generated configuration, configuring the http port to the picked one
-->
<exec executable="${java.home}/bin/java"
dir="${project.build.directory}"
spawn="true">
<arg value="-jar"/>
<arg value="${project.artifactId}-${project.version}-fat.jar"/>
<arg value="-conf"/>
<arg value="${project.build.directory}/test-classes/my-it-config.json"/>
</exec>
</target>
</configuration>
</execution>
<execution>
<id>stop-vertx-app</id>
<phase>post-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<!--
Kill the started process.
Finding the right process is a bit tricky. Windows command is in the windows profile (below)
-->
<target>
<exec executable="bash"
dir="${project.build.directory}"
spawn="false">
<arg value="-c"/>
<arg value="ps ax | grep -Ei '[\-]DtestPort=${http.port}\s+\-jar\s+${project.artifactId}' | awk 'NR==1{print $1}' | xargs kill -SIGTERM"/>
</exec>
</target>
</configuration>
</execution>
</executions>
</plugin>
這里有一大堆XML。我們?yōu)檫@個插件配置了兩個執(zhí)行階段狈涮。第一個狐胎,在pre-integration-test
階段,執(zhí)行一系列bash命令來啟動應(yīng)用歌馍。主要是執(zhí)行:
java -jar my-first-app-1.0-SNAPSHOT-fat.jar -conf .../my-it-config.json
fatfar被創(chuàng)建了握巢?
嵌入了我們應(yīng)用的fatfar在package階段被創(chuàng)建,在pre-integration-test
之前松却,所以暴浦,fatjar是被創(chuàng)建了。
如上晓锻,我們?nèi)缭谏a(chǎn)環(huán)境一樣啟動了應(yīng)用歌焦。
一旦集成測試被執(zhí)行了(第四步我們還沒說起),我們需要停止應(yīng)用(所以在post-integration-test
階段)砚哆。為了關(guān)閉應(yīng)用独撇,我們會使用一些shell魔法命令來查找我們的進程號,會用到ps命令并發(fā)送SIGTERM
信號躁锁,這些等同于:
ps
.... -> find your process id
kill your_process_id -SIGTERM
還有Windows纷铣?
我之前提起過,我們希望支持Windows而這些命令在Windows下不工作战转。不用擔心搜立,Windows配置在下文會提到...
我們現(xiàn)在將要做之前跳過的第四步。為了執(zhí)行我們的集成測試槐秧,我們將使用maven-failsafe-plugin
啄踊。將如下插件配置添加到你的pom.xml
文件中:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.18.1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemProperties>
<http.port>${http.port}</http.port>
</systemProperties>
</configuration>
</execution>
</executions>
</plugin>
如你所見忧设,我們將http.port屬性作為一個系統(tǒng)變量傳遞,這樣我們的測試能夠連接到正確的端口颠通。
就這樣了址晕,現(xiàn)在來試試(就Windows用戶而言,你必須更有耐心或直接跳到最后一節(jié))蒜哀。
mvn clean verify
我們不該使用 mvn integration-test
因為這樣應(yīng)用不會停止斩箫。verify
階段在post-integration-test
階段后吏砂,會分析集成測試的結(jié)果撵儿。由于集成測試失敗造成的構(gòu)建失敗會在這階段報告。
我們還沒有具體的集成測試內(nèi)容狐血!
我們準備好了集成測試所需的材料淀歇,但我們還沒有一個集成測試。為了簡化實現(xiàn)匈织,我們使用兩個庫:AssertJ 和Rest-Assured浪默。
AssertJ提供很多斷言,這些斷言你能夠鏈化并順暢使用缀匕。Rest Assured是一個用來測試REST API的框架纳决。
在pom.xml
文件中,在</dependencies>
前添加如下兩個依賴:
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
然后創(chuàng)建 src/test/java/io/vertx/blog/first/MyRestIT.java
文件乡小。不像單元測試阔加,集成測試以IT
結(jié)束。對Failsafe插件來說满钟,很容易區(qū)分單元測試(以Test開始結(jié)束)和集成測試(以IT開始結(jié)束)胜榔。在新增的文件中添加:
package io.vertx.blog.first;
import com.jayway.restassured.RestAssured;
import org.junit.AfterClass;
import org.junit.BeforeClass;
public class MyRestIT {
@BeforeClass
public static void configureRestAssured() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = Integer.getInteger("http.port", 8080);
}
@AfterClass
public static void unconfigureRestAssured() {
RestAssured.reset();
}
}
用@BeforeClass
和@AfterClass
注解的方法在類里所有的測試之前/之后分別執(zhí)行一次。這里湃番,我們只是取回http.port(作為系統(tǒng)參數(shù)傳入)并配置REST Assured夭织。
是時候?qū)崿F(xiàn)一個真的測試。讓我們檢測是否可以獲取某個特定產(chǎn)品:
@Test
public void checkThatWeCanRetrieveIndividualProduct() {
// Get the list of bottles, ensure it's a success and extract the first id.
final int id = RestAssured.get("/api/whiskies").then()
.assertThat()
.statusCode(200)
.extract()
.jsonPath().getInt("find { it.name=='Bowmore 15 Years Laimrig' }.id");
// Now get the individual resource and check the content
RestAssured.get("/api/whiskies/" + id).then()
.assertThat()
.statusCode(200)
.body("name", equalTo("Bowmore 15 Years Laimrig"))
.body("origin", equalTo("Scotland, Islay"))
.body("id", equalTo(id));
}
這里你能夠欣賞Rest Assured的力量和表達力吠撮。我們獲取產(chǎn)品列表尊惰,確認響應(yīng)是正確的,使用JSON(Groovy)路徑表達式來提取某個特定產(chǎn)品的id泥兰。
然后弄屡,我們嘗試獲取這個產(chǎn)品的元數(shù)據(jù),并檢驗結(jié)果逾条。
現(xiàn)在實現(xiàn)一個更復(fù)雜的場景琢岩。添加和刪除一個產(chǎn)品:
@Test
public void checkWeCanAddAndDeleteAProduct() {
// Create a new bottle and retrieve the result (as a Whisky instance).
Whisky whisky = RestAssured.given()
.body("{\"name\":\"Jameson\", \"origin\":\"Ireland\"}").request().post("/api/whiskies").thenReturn().as(Whisky.class);
Assertions.assertThat(whisky.getName()).isEqualToIgnoringCase("Jameson");
Assertions.assertThat(whisky.getOrigin()).isEqualToIgnoringCase("Ireland");
Assertions.assertThat(whisky.getId()).isNotZero();
// Check that it has created an individual resource, and check the content.
RestAssured.get("/api/whiskies/" + whisky.getId()).then()
.assertThat()
.statusCode(200)
.body("name", equalTo("Jameson"))
.body("origin", equalTo("Ireland"))
.body("id", equalTo(whisky.getId()));
// Delete the bottle
RestAssured.delete("/api/whiskies/" + whisky.getId()).then().assertThat().statusCode(204);
// Check that the resource is not available anymore
RestAssured.get("/api/whiskies/" + whisky.getId()).then()
.assertThat()
.statusCode(404);
}
現(xiàn)在我們有了集成測試,試著輸入如下命令:
mvn clean verify
還蠻簡單的师脂?等環(huán)境被準備好后是蠻簡單的担孔。江锨。。你能夠繼續(xù)實現(xiàn)其他集成測試來確保一切行為如你預(yù)期糕篇。
親愛的Windows用戶...
這一節(jié)是給Windows用戶的福利啄育,還有想在Windows機器上運行他們的集成測試的人們。之前我們執(zhí)行來停止應(yīng)用的命令在Windows系統(tǒng)上不起作用拌消。幸運的是挑豌,我們可以用一個在Windows系統(tǒng)上執(zhí)行的profile來擴展pom.xml。
在你的pom.xml文件中墩崩,緊跟著</build>氓英,添加:
<profiles>
<!-- A profile for windows as the stop command is different -->
<profile>
<id>windows</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>stop-vertx-app</id>
<phase>post-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<exec executable="wmic"
dir="${project.build.directory}"
spawn="false">
<arg value="process"/>
<arg value="where"/>
<arg value="CommandLine like '%${project.artifactId}%' and not name='wmic.exe'"/>
<arg value="delete"/>
</exec>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
這個profile用適用于Windows系統(tǒng)的版本替換了之前描述的版本來停止應(yīng)用。這個profile在Windows上自動啟用鹦筹。和在其他操作系統(tǒng)上一樣铝阐,執(zhí)行:
mvn clean verify
如果pom.xml配置文件有
Plugin execution not covered by lifecycle configuration:
org.codehaus.mojo:build-helper-maven-plugin:1.12:reserve-network-port
(execution: reserve-network-port, phase: process-sources)
這樣的報錯信息。
這是因為m2e對maven的階段支持不好造成的铐拐,具體可以參考m2e-execution-not-covered徘键。具體修正代碼如下:
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<versionRange>[${build-helper.maven-plugin.version},)</versionRange>
<goals>
<goal>reserve-network-port</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore/>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<article class="col-xs-12 blog-post">
<article>
結(jié)論
我們完成了...在這個帖子中,我們看到通過實現(xiàn)單元測試和集成測試遍蟋,我們對自己的Vert.x應(yīng)用更有信心了吹害。單元測試,由于vert.x-unit虚青,能夠檢測Vert.x應(yīng)用的異步特性它呀,但在復(fù)雜場景下可能太復(fù)雜。感謝Rest Assured和AssertJ挟憔,集成測試寫起來簡單很多...但是準備過程不夠直觀钟些。這篇帖子展示了如何配置集成測試環(huán)境。很明顯绊谭,你也能夠在單元測試中使用AssertJ和Rest Assured政恍。
在next post中,我們用一個數(shù)據(jù)庫來取代內(nèi)存后端达传,并和數(shù)據(jù)庫進行異步集成篙耗。
敬請期待!