在本章的前幾節(jié)中忙厌,您已經(jīng)構(gòu)建了一個在本地存儲數(shù)據(jù)的前端應(yīng)用程序烟号。由于您的客戶不共享后端瓶您,當多個用戶更新庫存時理肺,每個人都會看到不同的項目列表。
在本節(jié)中黎做,要在客戶端之間同步項目别厘,您將在第 4 章中將前端應(yīng)用程序與后端集成功咒,并學(xué)習(xí)如何測試該集成鞠值。在本節(jié)結(jié)束時媚创,您將擁有一個可以讀取、插入和更新數(shù)據(jù)庫項目的應(yīng)用程序彤恶。為了避免用戶必須刷新頁面才能看到其他人所做的更改,您還將實現(xiàn)實時更新鳄橘,這將通過 WebSockets 發(fā)生声离。
注意您可以在 https://github.com/lucasfcosta/testing-javascript-applications 找到上一章后端的完整代碼。
該后端將處理來自 Web 客戶端的請求瘫怜,為其提供數(shù)據(jù)并更新數(shù)據(jù)庫條目术徊。
為了讓本章專注于測試并確保服務(wù)器支持我們正在構(gòu)建的客戶端,我強烈建議您使用我推送到 GitHub 的后端應(yīng)用程序鲸湃。它已經(jīng)包含一些更新以更好地支持以下示例赠涮,因此您不必自己更改后端。
要運行它暗挑,請導(dǎo)航到chapter6/5_web_sockets_and_http_requests 中名為server 的文件夾笋除,使用npm install 安裝其依賴項,運行npm run migrate:dev 以確保您的數(shù)據(jù)庫具有最新架構(gòu)炸裆,并使用npm start 啟動它垃它。
如果您想自己更新后端,在服務(wù)器文件夾中有一個 README.md 文件烹看,其中詳細說明了我必須對我們在第 4 章中構(gòu)建的應(yīng)用程序所做的所有更改国拇。
6.5.1 涉及 HTTP 請求的測試
通過將用戶添加到庫存中的項目保存到數(shù)據(jù)庫來開始您的后端集成。為了實現(xiàn)這個功能惯殊,每當用戶添加一個項目時酱吝,向我從第 4 章添加到服務(wù)器的新 POST /inventory/:itemName 路由發(fā)送一個請求。這個請求的正文應(yīng)該包含添加的數(shù)量土思。
更新 addItem 函數(shù)务热,以便在用戶添加項目時向后端發(fā)送請求忆嗜,如下所示。
const data = { inventory: {} };
const API_ADDR = "http://localhost:3000";
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
fetch(`${API_ADDR}/inventory/${itemName}`, { ?
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
return data.inventory;
};
module.exports = { API_ADDR, data, addItem };
? 添加商品時向庫存發(fā)送 POST 請求
在您編寫從庫存中檢索項目的請求之前陕习,讓我們討論一下測試您剛剛實現(xiàn)的功能的最佳方法是什么霎褐。您將如何測試 addItem 函數(shù)是否與您的后端正確連接?
測試這種集成的一種次優(yōu)方法是啟動您的服務(wù)器并允許請求到達它该镣。乍一看冻璃,這似乎是最直接的選擇,但實際上损合,它需要更多的工作并產(chǎn)生更少的收益省艳。
必須運行后端才能讓客戶的測試通過會增加測試過程的復(fù)雜性,因為它涉及太多步驟并為人為錯誤創(chuàng)造了太多空間嫁审。開發(fā)人員很容易忘記他們必須運行服務(wù)器跋炕,甚至更容易忘記服務(wù)器應(yīng)該偵聽哪個端口或數(shù)據(jù)庫應(yīng)該處于哪種狀態(tài)。
盡管您可以自動執(zhí)行這些步驟律适,但最好避免它們辐烂。最好將這種集成留給端到端 UI 測試,您將在第 10 章中了解捂贿。通過避免必須使用后端來運行客戶端的測試纠修,您還可以更輕松地進行設(shè)置將在遠程環(huán)境中執(zhí)行測試的持續(xù)集成服務(wù),我將在第 12 章中介紹厂僧。
考慮到您不想讓后端參與這些測試扣草,您只有一種選擇:使用測試替身來控制要獲取的響應(yīng)。你可以通過兩種方式做到這一點:你可以存根 fetch 自身颜屠,編寫斷言以檢查它是否被充分使用辰妙,并指定一個硬編碼的響應(yīng)「撸或者您可以使用 nock 來代替服務(wù)器的必要性密浑。使用 nock,您可以確定要匹配哪些路由以及要提供哪些響應(yīng)蕴坪,從而使您的測試與實現(xiàn)細節(jié)更加分離肴掷,例如您傳遞給 fetch 的參數(shù),甚至您使用哪些庫來執(zhí)行請求背传。由于我之前在第 4 章中提到的這些優(yōu)點呆瞻,我建議您采用第二種選擇。
因為 nock 取決于到達攔截器的請求径玖,首先痴脾,請確保您的測試可以在節(jié)點內(nèi)運行并且它們可以分派請求。為此梳星,請運行您的測試赞赖,看看會發(fā)生什么滚朵。運行它們時,您會注意到所有調(diào)用 handleAddItem 的測試都將失敗前域,因為“未定義 fetch”辕近。
盡管 fetch 在瀏覽器上是全局可用的,但它尚未通過 JSDOM 可用匿垄,因此移宅,您需要找到一種方法來將其替換為等效的實現(xiàn)。要覆蓋它椿疗,您可以使用一個設(shè)置文件漏峰,該文件將 isomorphic-fetch(一種可以在 Node.js 中運行的 fetch 實現(xiàn))附加到全局命名空間。
使用 npm install --save-dev isomorphic-fetch 將 isomorphic-fetch 作為開發(fā)依賴項安裝届榄,并創(chuàng)建一個 setupGlobalFetch.js 文件浅乔,該文件會將其附加到全局命名空間。
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch; ?
創(chuàng)建此文件后铝条,將其添加到 jest.config.js 的 setupFilesAfterEnv 屬性中的腳本列表中靖苇,如下面的代碼所示,以便 Jest 可以在您的測試之前運行它班缰,使 fetch 對它們可用顾复。
module.exports = {
setupFilesAfterEnv: [
"<rootDir>/setupGlobalFetch.js",
"<rootDir>/setupJestDom.js"
]
};
在這些更改之后,如果您沒有可用的服務(wù)器鲁捏,您的測試應(yīng)該會失敗,因為 fetch 發(fā)出的請求無法得到響應(yīng)萧芙。
最后给梅,是時候使用 nock 來攔截對這些請求的響應(yīng)了。
將 nock 安裝為開發(fā)依賴項(npm install --save-dev nock)双揪,并更新您的測試动羽,以便它們具有 /inventory 路由的攔截器。
const nock = require("nock");
const { API_ADDR, addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
nock(API_ADDR) ?
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
? 響應(yīng)所有對 POST /inventory/:itemName 的 post 請求
嘗試僅為該文件運行測試渔期。 為此运吓,請將其名稱作為第一個參數(shù)傳遞給 Jest。 你會看到測試通過了疯趟。
現(xiàn)在拘哨,添加一個測試以確保已到達 POST /inventory/:itemName 的攔截器。
// ...
afterEach(() => {
if (!nock.isDone()) { ?
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
describe("addItem", () => {
// ...
test("sending requests when adding new items", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 5 }))
.reply(200);
addItem("cheesecake", 5);
});
});
? 如果在測試之后信峻,并非所有攔截器都已到達倦青,則清除它們并拋出錯誤
作為練習(xí),繼續(xù)使用 nock 攔截所有其他到達此路由的測試中對 POST /inventory/:itemName 的請求盹舞。如果您需要幫助产镐,請查看本書的 GitHub 存儲庫隘庄,網(wǎng)址為 https://github.com/lucasfcosta/testing-javascript-applications。
在更新其他測試時癣亚,不要忘記在多個集成級別檢查特定操作是否調(diào)用此路由丑掺。例如,我建議向 main.test.js 添加一個測試述雾,以確保在通過 UI 添加項目時到達正確的路由街州。
提示攔截器一旦到達就會被移除。為了避免測試因為 fetch 無法得到響應(yīng)而失敗绰咽,你必須在每次測試之前創(chuàng)建一個新的攔截器菇肃,或者使用 nock 的 persist 方法,正如我們在第 4 章中看到的取募。
要完成此功能琐谤,您的前端必須在加載時向服務(wù)器詢問庫存項目。更改后玩敏,只有在無法到達服務(wù)器時才應(yīng)將數(shù)據(jù)加載到 localStorage 中斗忌。
// ...
const { API_ADDR, data } = require("./inventoryController");
// ...
const loadInitialData = async () => {
try {
const inventoryResponse = await fetch(`${API_ADDR}/inventory`);
if (inventoryResponse.status === 500) throw new Error();
data.inventory = await inventoryResponse.json();
return updateItemList(data.inventory); ?
} catch (e) {
const storedInventory = JSON.parse( ?
localStorage.getItem("inventory")
);
if (storedInventory) {
data.inventory = storedInventory;
updateItemList(data.inventory);
}
}
};
module.exports = loadInitialData();
? 如果請求成功,則使用服務(wù)器的響應(yīng)更新項目列表
? 如果請求失敗旺聚,則從 localStorage 恢復(fù)庫存
即使您的應(yīng)用程序正在運行织阳,main.test.js 中檢查項目在會話之間是否持續(xù)的測試也應(yīng)該失敗。 它失敗是因為在嘗試從 localStorage 加載數(shù)據(jù)之前砰粹,它需要對 /inventory 的 GET 請求失敗唧躲。
要使該測試通過,您需要進行兩項更改:您必須使用 nock 使 GET /inventory 響應(yīng)錯誤碱璃,并且您必須等到初始數(shù)據(jù)加載完畢弄痹。
// ...
afterEach(nock.cleanAll);
test("persists items between sessions", async () => {
nock(API_ADDR) ?
.post(/inventory\/.*$/)
.reply(200);
nock(API_ADDR) ?
.get("/inventory")
.twice()
.replyWithError({ code: 500 });
// ...
document.body.innerHTML = initialHtml; ?
jest.resetModules();
await require("./main"); ?
// Assertions...
});
// ...
? 成功響應(yīng) POST /inventory/:itemName 請求
? 兩次回復(fù)錯誤請求到 GET /inventory
? 這相當于重新加載頁面。
? 等待初始數(shù)據(jù)加載
不要忘記這些測試包含一個 beforeEach 鉤子嵌器,因此肛真,在其中,您還必須等待 loadInitialData 完成爽航。
// ...
beforeEach(async () => {
document.body.innerHTML = initialHtml;
jest.resetModules();
nock(API_ADDR) ?
.get("/inventory")
.replyWithError({ code: 500 });
await require("./main");
jest.spyOn(window, "addEventListener");
});
// ...
? 回復(fù)錯誤請求到 GET /inventory
注意這里公開了應(yīng)用程序加載初始數(shù)據(jù)后將解決的承諾蚓让,因為您需要知道要等待什么。
或者讥珍,您可以等待測試中的固定超時历极,或繼續(xù)重試直到成功或超時。這些替代方案不會要求您導(dǎo)出 loadInitialData 返回的承諾串述,但它們會使您的測試變得不穩(wěn)定或比應(yīng)有的速度更慢执解。
您不必擔心 main.js 中 module.exports 的分配,因為在使用 Browserify 構(gòu)建后在瀏覽器中運行該文件時,它不會產(chǎn)生任何影響衰腌。 Browserify 將為您處理所有 module.exports 分配新蟆,將所有依賴項打包到一個 bundle.js 中。
既然您已經(jīng)學(xué)會了如何使用 nock 攔截器來測試涉及 HTTP 請求的功能右蕊,并在必要時覆蓋 fetch琼稻,我將以挑戰(zhàn)結(jié)束本節(jié)。
目前饶囚,在撤消操作時帕翻,您的應(yīng)用程序不會向服務(wù)器發(fā)送更新清單內(nèi)容的請求。作為練習(xí)萝风,嘗試使撤消功能與服務(wù)器同步嘀掸,并測試此集成。為了您能夠?qū)崿F(xiàn)此功能规惰,我在 GitHub 上本章的服務(wù)器文件夾中添加了一個新的 DELETE /inventory/:itemName 路由到服務(wù)器睬塌,該路由包含一個包含用戶想要刪除的數(shù)量的正文。
在本節(jié)結(jié)束時歇万,您應(yīng)該能夠通過使用 nock 準確模擬客戶端的行為揩晴,將客戶端的測試與后端隔離。多虧了 nock贪磺,您可以專注于指定您的服務(wù)器在哪種情況下會產(chǎn)生的響應(yīng)硫兰,而無需啟動整個后端。創(chuàng)建這樣的隔離測試可以讓團隊中的每個人更快寒锚、更輕松地運行測試劫映。這種改進加速了開發(fā)人員收到的反饋循環(huán),因此刹前,激勵他們編寫更好的測試并更頻繁地進行測試苏研,這反過來往往會導(dǎo)致更可靠的軟件。
6.5.2 涉及 WebSocket 的測試
到目前為止腮郊,如果您的應(yīng)用程序一次只有一個用戶,它可以無縫運行筹燕。但是轧飞,如果多個操作員需要同時管理庫存怎么辦?如果是這種情況撒踪,庫存很容易不同步过咬,導(dǎo)致每個操作員看到不同的項目和數(shù)量。
為了解決這個問題制妄,您將通過 WebSockets 實現(xiàn)對實時更新的支持掸绞。這些 WebSocket 將負責(zé)在庫存數(shù)據(jù)更改時更新每個客戶端,以便在客戶端之間始終保持同步。
因為這本書是關(guān)于測試的衔掸,我已經(jīng)在后端實現(xiàn)了這個功能烫幕。如果你不想自己實現(xiàn)它,你可以使用你可以在本書的 GitHub 存儲庫中的第 6 章文件夾中找到的服務(wù)器敞映,網(wǎng)址為 https://github.com/lucasfcosta/testing-javascript-applications较曼。
當客戶端添加項目時,我對服務(wù)器所做的更改將導(dǎo)致它向所有連接的客戶端發(fā)出 add_item 事件振愿,除了發(fā)送請求的客戶端捷犹。
要連接到服務(wù)器,您將使用 socket.io-client 模塊冕末,因此您必須使用 npm install socket.io-client 將其安裝為依賴項萍歉。
通過創(chuàng)建將連接到服務(wù)器并在連接后保存客戶端 ID 的模塊,開始實現(xiàn)實時更新功能档桃。
const { API_ADDR } = require("./inventoryController");
const client = { id: null };
const io = require("socket.io-client");
const connect = () => {
return new Promise(resolve => {
const socket = io(API_ADDR); ?
socket.on("connect", () => { ?
client.id = socket.id;
resolve(socket);
});
});
}
module.exports = { client, connect };
? 創(chuàng)建一個連接到 API_ADDR 的客戶端實例
? 客戶端連接后枪孩,存儲其 id 并解析 promise
每個客戶端要連接到服務(wù)器,必須在 main.js 中調(diào)用 socket.js 導(dǎo)出的 connect 函數(shù)胳蛮。
const { connect } = require("./socket");
// ...
connect(); ?
module.exports = loadInitialData();
? 應(yīng)用程序加載時連接到 Socket.io 服務(wù)器
客戶端連接到服務(wù)器后销凑,每當用戶添加新項目時,客戶端必須通過 x-socket-client-id 頭將其 Socket.io 客戶端 ID 發(fā)送到服務(wù)器仅炊。 服務(wù)器將使用此標頭來識別哪個客戶端添加了該項目斗幼,以便它可以跳過它,因為該客戶端已經(jīng)更新了自己抚垄。
注意 允許客戶端向庫存添加項目的路由將提取 x-socket-client-id 標頭中的值以確定哪個客戶端發(fā)送了請求蜕窿。 然后,一旦將項目添加到清單中呆馁,它將遍歷所有連接的套接字并向 id 與 x-socket-client-id 中的不匹配的客戶端發(fā)出 add_item 事件桐经。
router.post("/inventory/:itemName", async ctx => {
const clientId = ctx.request.headers["x-socket-client-id"];
// ...
Object.entries(io.socket.sockets.connected)
.forEach(([id, socket]) => {
if (id === clientId) return;
socket.emit("add_item", { itemName, quantity });
});
// ...
});
更新inventoryController.js,以便它將客戶端的ID發(fā)送到服務(wù)器浙滤,如下所示阴挣。
// ...
const addItem = (itemName, quantity) => {
const { client } = require("./socket");
// ...
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-socket-client-id": client.id ?
},
body: JSON.stringify({ quantity })
});
return data.inventory;
};
? 包含一個 x-socket-client-id,其中包含 Socket.io 客戶端在發(fā)送添加項目的請求時的 ID
現(xiàn)在服務(wù)器可以識別發(fā)送者纺腊,最后一步是更新 socket.js 文件畔咧,以便客戶端可以在收到其他人添加項目時服務(wù)器發(fā)送的 add_item 消息時更新自己。 這些消息包含一個 itemName 和一個數(shù)量屬性揖膜,您將使用它們來更新庫存數(shù)據(jù)誓沸。 一旦本地狀態(tài)是最新的,您將使用它來更新 DOM壹粟。
const { API_ADDR, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
// ...
const handleAddItemMsg = ({ itemName, quantity }) => { ?
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return updateItemList(data.inventory);
};
const connect = () => {
return new Promise(resolve => {
// ...
socket.on("add_item", handleAddItemMsg); ?
});
};
module.exports = { client, connect };
? 一個函數(shù)拜隧,用于更新應(yīng)用程序的狀態(tài)和給定包含項目名稱和數(shù)量的對象的項目列表
? 當服務(wù)器發(fā)出 add_item 事件時調(diào)用 handleAddItemMsg
通過 npm run build 使用 Browserify 重建您的 bundle.js 并使用 npx http-server ./ 為其提供服務(wù),來嘗試這些更改。 不要忘記您的服務(wù)器必須在 API_ADDR 中指定的地址上運行洪添。
可以在多個集成級別上測試此功能垦页。 例如,您可以單獨檢查您的 handleAddItemMsg 函數(shù)薇组,而根本不涉及 WebSockets外臂。
要單獨測試 handleAddItemMsg,首先在 socket.js 中導(dǎo)出它律胀。
// ...
module.exports = { client, connect, handleAddItemMsg };
然后宋光,在新的socket.test.js中導(dǎo)入,直接調(diào)用炭菌,傳入一個包含itemName和數(shù)量的對象罪佳。 不要忘記您需要掛鉤來確保在每次測試之前重置文檔和庫存狀態(tài)。
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText } = require("@testing-library/dom");
const { data } = require("./inventoryController");
const { handleAddItemMsg } = require("./socket");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
beforeEach(() => {
data.inventory = {};
});
describe("handleAddItemMsg", () => {
test("updating the inventory and the item list", () => {
handleAddItemMsg({ itemName: "cheesecake", quantity: 6 }); ?
expect(data.inventory).toEqual({ cheesecake: 6 });
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(1);
expect(getByText(itemList, "cheesecake - Quantity: 6"))
.toBeInTheDocument();
});
});
? 調(diào)用handleAddItemMsg函數(shù)直接測試
提示 盡管此測試對您在迭代時獲得反饋很有用黑低,但它與通過 WebSockets 發(fā)送 add_item 消息而不是直接調(diào)用 handleAddItemMsg 的測試高度重疊赘艳。因此,在實際場景中克握,在選擇是否保留之前蕾管,請考慮您的時間和成本限制。
正如我之前提到的菩暗,準確復(fù)制運行時場景將使您的測試產(chǎn)生更可靠的保證掰曾。在這種情況下,您可以模擬后端發(fā)送的更新最接近的是創(chuàng)建一個 Socket.io 服務(wù)器并自己調(diào)度更新停团。然后旷坦,您可以檢查這些更新是否在您的客戶端中觸發(fā)了所需的效果。
因為在運行測試時需要 Socket.io 服務(wù)器佑稠,所以使用 npm install --save-dev socket.io 將其安裝為開發(fā)依賴項秒梅。
安裝 Socket.io 后,創(chuàng)建一個名為 testSocketServer.js 的文件舌胶,您將在其中創(chuàng)建自己的 Socket.io 服務(wù)器捆蜀。此文件應(yīng)導(dǎo)出用于啟動和停止服務(wù)器的函數(shù)以及向客戶端發(fā)送消息的函數(shù)。
const server = require("http").createServer();
const io = require("socket.io")(server); ?
const sendMsg = (msgType, content) => { ?
io.sockets.emit(msgType, content);
};
const start = () => ?
new Promise(resolve => {
server.listen(3000, resolve);
});
const stop = () => ?
new Promise(resolve => {
server.close(resolve);
});
module.exports = { start, stop, sendMsg };
? 創(chuàng)建一個socket.io服務(wù)器
? 向連接到socket.io服務(wù)器的客戶端發(fā)送消息的函數(shù)
? 在端口 3000 上啟動 socket.io 服務(wù)器幔嫂,并在它啟動后解析一個 promise
? 關(guān)閉 socket.io 服務(wù)器漱办,并在停止后解析承諾
注意 理想情況下,您應(yīng)該有一個單獨的常量來確定您的服務(wù)器應(yīng)該偵聽的端口婉烟。如果需要,您可以將 API_ADDR 分成 API_HOST 和 API_PORT暇屋。因為本書側(cè)重于測試似袁,所以我在這里對 3000 進行了硬編碼。
此外,為了避免由于服務(wù)器已綁定到端口 3000 而無法運行測試昙衅,允許用戶通過環(huán)境變量配置此端口可能會很有用扬霜。
返回在 start 和 stop 結(jié)束時解析的 promise 是至關(guān)重要的,這樣您就可以在鉤子中使用它們時等待它們完成而涉。否則著瓶,您的測試可能會因資源掛起而掛起。
最后啼县,是時候編寫一個測試材原,通過 Socket.io 服務(wù)器發(fā)送消息并檢查您的應(yīng)用程序是否正確處理它們。
從將啟動服務(wù)器的鉤子開始季眷,將您的客戶端連接到它余蟹,然后在測試完成后關(guān)閉服務(wù)器。
const nock = require("nock");
// ...
const { start, stop } = require("./testSocketServer");
// ...
describe("handling real messages", () => {
beforeAll(start); ?
beforeAll(async () => {
nock.cleanAll(); ?
await connect(); ?
});
afterAll(stop); ?
});
? 在測試運行之前子刮,啟動你的 Socket.io 測試服務(wù)器
? 為避免 nock 干擾您與 Socket.io 服務(wù)器的連接威酒,請在嘗試連接之前清除所有模擬
? 在所有測試之前,連接到 Socket.io 測試服務(wù)器
? 測試完成后挺峡,停止 Socket.io 測試服務(wù)器
最后葵孤,編寫一個發(fā)送 add_item 消息的測試,等待一秒鐘以便客戶端可以接收和處理它橱赠,并檢查新的應(yīng)用程序狀態(tài)是否與您期望的狀態(tài)相匹配尤仍。
const { start, stop, sendMsg } = require("./testSocketServer");
// ...
describe("handling real messages", () => {
// ...
test("handling add_item messages", async () => {
sendMsg("add_item", { itemName: "cheesecake", quantity: 6 }); ?
await new Promise(resolve => setTimeout(resolve, 1000)); ?
expect(data.inventory).toEqual({ cheesecake: 6 }); ?
const itemList = document.getElementById("item-list"); ?
expect(itemList.childNodes).toHaveLength(1); ?
expect(getByText(itemList, "cheesecake - Quantity: 6")) ?
.toBeInTheDocument(); ?
});
});
? 通過 Socket.io 測試服務(wù)器發(fā)送消息
? 等待消息被處理
? 檢查頁面狀態(tài)是否符合預(yù)期狀態(tài)
請注意此測試與 handleAddItemMsg 的單元測試有多少重疊。兩者兼有的好處是病线,如果連接設(shè)置出現(xiàn)問題吓著,使用真實套接字的測試將失敗,但單元測試不會送挑。因此绑莺,您可以快速檢測問題是出在邏輯上還是出在服務(wù)器連接上。兩者兼而有之的問題在于它們會增加維護測試套件的額外成本惕耕,特別是考慮到您在兩個測試中執(zhí)行相同的斷言纺裁。
現(xiàn)在您已經(jīng)檢查了您的應(yīng)用程序在接收消息時是否可以更新,編寫一個測試來檢查庫存控制器.js 中的 handleAddItem 函數(shù)是否將套接字客戶端的 ID 包含在它發(fā)送到服務(wù)器的 POST 請求中司澎。該測試不同部分之間的通信如圖 6.9 所示欺缘。
為此,您必須啟動您的測試服務(wù)器挤安,連接到它谚殊,并針對 nock 攔截器執(zhí)行 handleAddItem 函數(shù),它將僅匹配包含足夠 x-socket-client-id 標頭的請求蛤铜。
// ...
const { start, stop } = require("./testSocketServer");
const { client, connect } = require("./socket");
// ...
describe("live-updates", () => {
beforeAll(start);
beforeAll(async () => {
nock.cleanAll();
await connect();
});
afterAll(stop);
test("sending a x-socket-client-id header", () => {
const clientId = client.id;
nock(API_ADDR, { ?
reqheaders: { "x-socket-client-id": clientId }
})
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
});
});
? 僅成功響應(yīng)包含 x-socket-client-id 標頭的 POST /inventory/:itemName 請求
重要的是要看到嫩絮,在這些示例中丛肢,我們并沒有試圖在測試中復(fù)制后端的行為。我們分別檢查我們發(fā)送的請求和我們是否可以處理收到的消息剿干。檢查后端是否將正確的消息發(fā)送到正確的客戶端是應(yīng)該在服務(wù)器的測試中進行的驗證蜂怎,而不是客戶端的測試。
既然您已經(jīng)了解了如何設(shè)置可在測試中使用的 Socket.io 服務(wù)器以及如何驗證 WebSockets 集成置尔,請嘗試使用新功能擴展此應(yīng)用程序并對其進行測試杠步。請記住,您可以通過單獨檢查處理程序函數(shù)或通過測試服務(wù)器推送真實消息來在多個不同的集成級別編寫這些測試榜轿。例如幽歼,嘗試在客戶端單擊撤消按鈕時推送實時更新,或者嘗試添加一個測試來檢查頁面加載時 main.js 是否連接到服務(wù)器差导。
以 WebSocket 為例试躏,您一定已經(jīng)學(xué)會了如何模擬前端可能與其他應(yīng)用程序進行的其他類型的交互。如果您有存根會導(dǎo)致過多維護開銷的依賴項设褐,則最好實現(xiàn)您自己的依賴項實例——您可以完全控制的實例颠蕴。例如,在這種情況下助析,手動操作多個不同的間諜來訪問偵聽器和觸發(fā)事件會導(dǎo)致過多的維護開銷犀被。除了使您的測試更難閱讀和維護之外,它還會使它們與運行時發(fā)生的情況更加不同外冀,從而導(dǎo)致您的可靠性保證更弱寡键。這種方法的缺點是您的測試范圍會增加,從而使您獲得反饋的時間更長雪隧,并且變得更加粗糙西轩。因此,在決定適合您情況的最佳技術(shù)時脑沿,您必須謹慎藕畔。
概括
JavaScript 在瀏覽器中可以訪問的值和 API 與它在 Node.js 中可以訪問的值和 API 不同。因為 Jest 只能在 Node.js 中運行庄拇,所以在使用 Jest 運行測試時注服,您必須準確地復(fù)制瀏覽器的環(huán)境。
為了模擬瀏覽器的環(huán)境措近,Jest 使用了 JSDOM溶弟,這是一種完全用 JavaScript 編寫的 Web 標準的實現(xiàn)。 JSDOM 使您可以在其他運行時環(huán)境(如 Node.js)中訪問瀏覽器 API瞭郑。
在多個集成級別編寫測試需要您將代碼組織成單獨的部分辜御。為了在測試中輕松管理不同的模塊,您仍然可以使用 require屈张,但是您必須使用像 Browserify 或 Webpack 這樣的打包器將您的依賴項打包到可以在瀏覽器中運行的文件中擒权。
在您的測試中苇本,感謝 JSDOM,您可以訪問諸如 document.querySelector 和 document.getElementById 之類的 API菜拓。一旦你使用了你想要測試的函數(shù),就可以使用這些 API 在頁面中的 DOM 節(jié)點上查找和斷言笛厦。
通過 ID 或它們在 DOM 中的位置查找元素可能會導(dǎo)致您的測試變得脆弱并且與您的標記耦合得太緊纳鼎。為避免這些問題,請使用諸如 dom-testing-library 之類的工具通過元素的內(nèi)容或其他屬性來查找元素裳凸,這些屬性是元素應(yīng)有的組成部分贱鄙,例如其角色或標簽。
為了編寫更準確和可讀的斷言姨谷,而不是手動訪問 DOM 元素的屬性或編寫復(fù)雜的代碼來執(zhí)行某些檢查逗宁,而是使用 jest-dom 之類的庫來擴展 Jest,并使用專門針對 DOM 的新斷言梦湘。
瀏覽器對復(fù)雜的用戶交互做出反應(yīng)瞎颗,例如打字、點擊和滾動捌议。為了處理這些交互哼拔,瀏覽器依賴于事件。由于測試在準確模擬運行時發(fā)生的情況時更可靠瓣颅,因此您的測試應(yīng)盡可能精確地模擬事件倦逐。
準確重現(xiàn)事件的一種方法是使用來自 dom-testing-library 的 fireEvent 函數(shù)或由 test-library 組織下的另一個庫 user-event 提供的實用程序。
您可以在不同的集成級別測試事件及其處理程序宫补。如果您在編寫代碼時需要更精細的反饋檬姥,可以通過直接調(diào)用處理程序來測試它們。如果您想用細粒度的反饋換取更可靠的保證粉怕,您可以改為發(fā)送真實事件健民。
如果您的應(yīng)用程序使用諸如 History 或 Web Storage API 之類的 Web API,您可以在測試中使用它們的 JSDOM 實現(xiàn)斋荞。請記住荞雏,您不應(yīng)自己測試這些 API;您應(yīng)該測試您的應(yīng)用程序是否與它們充分交互平酿。
為了避免使您的測試設(shè)置過程更加復(fù)雜凤优,并擺脫啟動后端以運行前端測試的必要性,請使用 nock 攔截請求蜈彼。使用 nock筑辨,您可以確定攔截哪些路由以及這些攔截器將產(chǎn)生哪些響應(yīng)。
與我們見過的所有其他類型的測試類似幸逆,WebSockets 可以在不同的集成級別進行測試棍辕。您可以編寫直接調(diào)用處理程序函數(shù)的測試暮现,或者您可以創(chuàng)建一個服務(wù)器,通過它您將發(fā)送真實的消息楚昭。