Mirage JS 深入探討:使用 Mirage JS 和 Cypress 進行 UI 測試(第 4 部分)
已發表: 2022-03-10我最喜歡的關於軟件測試的引用之一來自 Flutter 文檔。 它說:
“當您添加更多功能或更改現有功能時,如何確保您的應用程序繼續運行? 通過編寫測試。”
關於這一點,Mirage JS Deep Dive 系列的最後一部分將重點介紹使用 Mirage 測試您的 JavaScript 前端應用程序。
注意:本文假設 Cypress 環境。 Cypress 是一個用於 UI 測試的測試框架。 但是,您可以將這裡的知識轉移到您使用的任何 UI 測試環境或框架中。
閱讀該系列的前幾部分:
- 第 1 部分:了解 Mirage JS 模型和關聯
- 第 2 部分:了解工廠、夾具和序列化程序
- 第 3 部分:了解時序、響應和直通
UI 測試入門
UI 或用戶界面測試是一種驗收測試形式,用於驗證前端應用程序的用戶流程。 這類軟件測試的重點是最終用戶,也就是將在各種設備(從台式機、筆記本電腦到移動設備)上與您的 Web 應用程序交互的實際人。 這些用戶將使用鍵盤、鼠標或觸摸屏等輸入設備與您的應用程序交互或交互。 因此,編寫 UI 測試是為了盡可能地模擬用戶與您的應用程序的交互。
我們以電子商務網站為例。 一個典型的 UI 測試場景是:
- 用戶在訪問主頁時可以查看產品列表。
其他 UI 測試場景可能是:
- 用戶可以在產品的詳細信息頁面上看到產品的名稱。
- 用戶可以點擊“加入購物車”按鈕。
- 用戶可以結賬。
你明白了,對吧?
在進行 UI 測試時,您將主要依賴後端狀態,即它是返回產品還是錯誤? Mirage 在其中扮演的角色是讓您可以根據需要調整這些服務器狀態。 因此,您無需在 UI 測試中向生產服務器發出實際請求,而是向 Mirage 模擬服務器發出請求。
對於本文的剩餘部分,我們將在一個虛構的電子商務 Web 應用程序 UI 上執行 UI 測試。 所以讓我們開始吧。
我們的第一個 UI 測試
如前所述,本文假設 Cypress 環境。 賽普拉斯使 Web 上的 UI 測試變得快速而簡單。 您可以模擬點擊和導航,並且可以以編程方式訪問應用程序中的路線。 有關賽普拉斯的更多信息,請參閱文檔。
因此,假設我們可以使用 Cypress 和 Mirage,讓我們首先為您的 API 請求定義一個代理函數。 我們可以在 Cypress 設置的support/index.js
文件中這樣做。 只需將以下代碼粘貼到:
// cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })
然後,在您的應用引導文件(Vue 為main.js
,React 為index.js
)中,我們將僅在賽普拉斯運行時使用 Mirage 將應用的 API 請求代理到handleFromCypress
函數。 這是代碼:
import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }
通過該設置,無論何時賽普拉斯運行,您的應用都知道使用 Mirage 作為所有 API 請求的模擬服務器。
讓我們繼續編寫一些 UI 測試。 我們將首先測試我們的主頁,看看它是否顯示了5 個產品。 要在 Cypress 中執行此操作,我們需要在項目目錄根目錄的tests
文件夾中創建一個homepage.test.js
文件。 接下來,我們將告訴賽普拉斯執行以下操作:
- 訪問主頁ie
/
route - 然後斷言它是否具有
product
類的 li 元素,並檢查它們的數量是否為 5。
這是代碼:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
您可能已經猜到此測試會失敗,因為我們沒有生產服務器將 5 個產品返回到我們的前端應用程序。 那麼我們該怎麼辦? 我們在 Mirage 中模擬了服務器! 如果我們引入 Mirage,它可以攔截我們測試中的所有網絡調用。 讓我們在下面執行此操作,並在 beforeEach 函數中的每個測試之前啟動 Mirage 服務器,並在beforeEach
函數afterEach
其關閉。 beforeEach
和afterEach
函數都由 Cypress 提供,它們是可用的,因此您可以在測試套件中的每個測試運行之前和之後運行代碼——因此得名。 所以讓我們看一下代碼:
// homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })
好的,我們正在取得進展; 我們已經從 Mirage 導入了服務器,我們分別在beforeEach
和afterEach
函數中啟動和關閉它。 讓我們開始模擬我們的產品資源。
// homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
注意:如果您不了解上述代碼片段的 Mirage 位,可以隨時查看本系列的前幾部分。
- 第 1 部分:了解 Mirage JS 模型和關聯
- 第 2 部分:了解工廠、夾具和序列化程序
- 第 3 部分:了解時序、響應和直通
好的,我們已經開始通過創建產品模型以及為/api/products
路由創建路由處理程序來充實我們的服務器實例。 但是,如果我們運行測試,它將失敗,因為我們在 Mirage 數據庫中還沒有任何產品。
讓我們用一些產品填充 Mirage 數據庫。 為了做到這一點,我們可以在我們的服務器實例上使用create()
方法,但是手動創建 5 個產品似乎相當乏味。 應該有更好的方法。
啊,是的,有。 讓我們利用工廠(如本系列第二部分所述)。 我們需要像這樣創建我們的產品工廠:
// homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
然後,最後,我們將使用createList()
快速創建我們的測試需要通過的 5 個產品。
我們開工吧:
// homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });
因此,當我們運行測試時,它通過了!
注意:每次測試後,Mirage 的服務器都會關閉並重置,因此此狀態不會在測試中洩漏。
避免使用多個 Mirage 服務器
如果您一直關注本系列,您會注意到我們在開發中使用 Mirage 攔截我們的網絡請求; 我們在應用程序的根目錄中有一個server.js
文件,我們在其中設置了 Mirage。 本著 DRY(不要重複自己)的精神,我認為利用該服務器實例而不是為開發和測試使用兩個單獨的 Mirage 實例會更好。 為此(如果您還沒有server.js
文件),只需在您的項目src目錄中創建一個。

注意:如果您使用的是 JavaScript 框架,您的結構會有所不同,但總體思路是在項目的 src 根目錄中設置 server.js 文件。
因此,使用這個新結構,我們將在server.js
中導出一個函數,該函數負責創建我們的 Mirage 服務器實例。 讓我們這樣做:
// src/server.js export function makeServer() { /* Mirage code goes here */}
讓我們通過移除我們在homepage.test.js
中創建的 Mirage JS 服務器並將其添加到makeServer
函數體中來完成makeServer
函數的實現:
import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }
現在您所要做的就是在您的測試中導入makeServer
。 使用單個 Mirage Server 實例更簡潔; 這樣您就不必為開發和測試環境維護兩個服務器實例。
導入makeServer
函數後,我們的測試現在應該如下所示:
import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });
因此,我們現在擁有了一個中央 Mirage 服務器,為我們提供開發和測試服務。 您還可以使用makeServer
函數在開發中啟動 Mirage(請參閱本系列的第一部分)。
您的 Mirage 代碼不應進入生產環境。 因此,根據您的構建設置,您只需在開發模式下啟動 Mirage。
注意:閱讀我關於如何使用 Mirage 和 Vue.js 設置 API 模擬的文章,了解我是如何在 Vue 中做到這一點的,這樣你就可以在你使用的任何前端框架中進行複制。
測試環境
Mirage 有兩個環境:開發(默認)和測試。 在開發模式下,Mirage 服務器的默認響應時間為 400 毫秒(您可以自定義。請參閱本系列的第三篇文章),將所有服務器響應記錄到控制台,並加載開發種子。
但是,在測試環境中,我們有:
- 0 延遲以保持我們的測試快速
- Mirage 會抑制所有日誌,以免污染您的 CI 日誌
- Mirage 還將忽略 seed
seeds()
函數,以便您的種子數據可以僅用於開發,但不會洩漏到您的測試中。 這有助於保持測試的確定性。
讓我們更新我們的makeServer
,以便我們可以從測試環境中受益。 為此,我們將使它接受一個帶有 environment 選項的對象(我們將默認它為 development 並在我們的測試中覆蓋它)。 我們的server.js
現在應該如下所示:
// src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }
另請注意,我們使用 ES6 屬性簡寫將環境選項傳遞給 Mirage 服務器實例。 現在有了這個,讓我們更新我們的測試以覆蓋要測試的環境值。 我們的測試現在看起來像這樣:
import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });
AAA 測試
Mirage 鼓勵採用稱為 AAA 或 AAA 測試方法的測試標準。 這代表Arrange 、 Act和Assert 。 你已經可以在我們上面的測試中看到這個結構了:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
您可能需要打破這種模式,但 10 次中有 9 次它應該對您的測試正常工作。
讓我們測試錯誤
到目前為止,我們已經測試了我們的主頁是否有 5 個產品,但是,如果服務器關閉或獲取產品出現問題怎麼辦? 在這種情況下,我們不需要等待服務器關閉來處理我們的 UI 的外觀。 我們可以簡單地用 Mirage 模擬這種情況。
當用戶在主頁上時,讓我們返回 500(服務器錯誤)。 正如我們在上一篇文章中看到的,為了自定義 Mirage 響應,我們使用了 Response 類。 讓我們導入它並編寫我們的測試。
homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });
多麼靈活的世界! 我們只是覆蓋 Mirage 將返回的響應,以便測試我們的 UI 在獲取產品失敗時將如何顯示。 我們的整個homepage.test.js
文件現在看起來像這樣:
// homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });
請注意,我們對/api/products
處理程序所做的修改只存在於我們的測試中。 這意味著它在您處於開發模式時按照我們之前定義的方式工作。
因此,當我們運行測試時,兩者都應該通過。
注意:我認為值得一提的是,我們在 Cypress 中查詢的元素應該存在於您的前端 UI 中。 賽普拉斯不會為您創建 HTML 元素。
測試產品詳細信息頁面
最後,我們來測試一下商品詳情頁的UI。 所以這就是我們正在測試的:
- 用戶可以在產品詳情頁面看到產品名稱
讓我們開始吧。 首先,我們創建一個新的測試來測試這個用戶流。
這是測試:
it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });
你的homepage.test.js
最終應該是這樣的。
// homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });
當您運行測試時,這三個都應該通過。
包起來
在本系列中向您展示 Mirage JS 的內部非常有趣。 通過使用 Mirage 模擬後端服務器,我希望您能夠更好地開始擁有更好的前端開發體驗。 我也希望您能利用本文中的知識為您的前端應用程序編寫更多的驗收/UI/端到端測試。
- 第 1 部分:了解 Mirage JS 模型和關聯
- 第 2 部分:了解工廠、夾具和序列化程序
- 第 3 部分:了解時序、響應和直通
- 第 4 部分:使用 Mirage JS 和 Cypress 進行 UI 測試