第 9 部分 – 測試
在最後這部分,我們將學習如何在各種不同的伺服器狀態下,使用 Mirage 來測試我們的應用程式。
我們的專案已設定好 Jest 和 Testing Library。我們也提供了一個 visit(url)
輔助函式,可將我們的提醒應用程式在指定的 URL 上呈現出來。
讓我們開啟 src/__tests__/app.js
,並撰寫我們的第一個測試。
我們想驗證當沒有提醒事項時,我們的應用程式會顯示「全部完成!」。以下是此測試的程式碼 – 請複製並貼到您的專案中
// __tests__/app.js
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
test("it shows a message when there are no reminders", async () => {
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
現在開啟一個新的終端機視窗,執行 yarn test
。Jest 應該會啟動一個監看程式,在您每次變更時重新執行您的測試。
在測試完成首次執行後,您應該會看到錯誤訊息
找不到具有文字的元素:「全部完成!」
您應該也會看到一個錯誤訊息,指出「網路請求失敗」。
如果您查看除錯輸出,您甚至會看到本教學第 1 部分中熟悉的網路錯誤 UI 出現在 DOM 中
如同第 1 部分,這是因為我們的應用程式正在對 /api/reminders
進行初始提取,但沒有伺服器可以回應它。現在是時候將我們的 Mirage 伺服器帶入測試中了。
讓我們匯入我們的 makeServer
函式,並在測試開始時執行它
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"
test("it shows a message when there are no reminders", async () => {
makeServer()
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
我們的測試仍然失敗,但如果我們捲動瀏覽除錯輸出,我們不會再看到關於失敗網路請求的訊息。
相反地,我們會看到 DOM 正在呈現我們在教學前一部分的 seeds()
鉤子中建立的現有提醒事項
這是合理的,因為我們正在建立與開發中相同的 Mirage 伺服器,而且我們使用這五個提醒事項來為該伺服器設定種子,以協助我們開發應用程式。
因此,我們希望在此有一個新的 Mirage 伺服器執行個體,其中沒有任何資料,這樣我們才能驗證空狀態會顯示「全部完成!」訊息。我們可以完成此操作的一種方式是返回我們的伺服器定義,並刪除我們所有的 seeds()
資料。
但是,有更好的方法,它不需要變更我們的開發種子:我們可以利用 environment
選項,在「測試」模式中啟動我們的 Mirage 伺服器。
若要查看其運作方式,請返回您的 server.js
檔案,並新增 environment: 'test'
選項
// server.js
import { createServer } from "miragejs"
export default function () {
return createServer({
environment: "test",
// rest of server
})
}
儲存該變更,並且在您的測試重新執行之後,它應該會通過!
那麼,"test"
環境對我們的伺服器有何影響?有幾個地方:它會將 timing
設定為 0,讓我們的測試能快速執行;它會隱藏 Mirage 的記錄,讓您的測試輸出保持乾淨;而且最重要的是,它會略過 seeds()
鉤子。
因此,我們可以重複使用我們所有的模型、序列化器、工廠和路由,但將 seeds()
資料設定分開用於開發模式。在測試中,我們會使用每個測試來以該測試所需的確切狀態來設定我們伺服器的資料。
對於我們目前的測試,我們實際上希望資料庫是空的,因此我們只需要啟動伺服器,而無需建立任何額外的資料。這讓 Mirage 可以正確地處理 GET API 請求,並以空的資料集回應。現在,我們的「全部完成」訊息斷言通過,這正是我們針對此測試所希望的。
現在,如果您切換回開發伺服器 (或在另一個終端機視窗中執行 yarn start
),您會注意到我們在 seeds()
中建立的提醒事項不再顯示。這是因為我們的開發伺服器現在也以測試模式執行。
讓我們透過更新我們從 server.js
匯出的函式來修正此問題,使其接收環境引數,並且我們會使用它來設定我們伺服器的環境
export default function (environment = "development") {
return createServer({
environment,
// ...rest of server
})
}
現在我們的開發伺服器再次使用我們的種子,並且具有人工延遲和主控台記錄來協助我們開發,但我們的測試應該會再次失敗。
回到我們的測試檔案中,我們會更新對 makeServer
的呼叫,並傳入「測試」的環境
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"
test("it shows a message when there are no reminders", async () => {
makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
現在我們的測試通過,但我們的開發環境仍然具有其獨立的種子資料,可協助我們在開發期間!
這只是設定 Mirage 的一種方式。在您自己的專案中,如何設定引數以及選擇預設值取決於您。但重點是,您應該在開發和測試環境之間共用您的 Mirage 伺服器。
現在我們有一種在測試中建立獨立 Mirage 伺服器的簡單方法,讓我們繼續進行,看看當伺服器上已存在三個提醒事項時,如何為我們的 UI 撰寫測試。
我們會從複製和貼上我們先前的測試開始,並更新描述
test("it shows existing reminders", async () => {
makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
當您儲存時,Jest 會執行兩個測試,而且您應該會看到下列警告
在已經有一個正在執行的 Pretender 執行個體時,您建立了第二個 Pretender 執行個體
Mirage 在幕後使用 Pretender 程式庫,而 Pretender 告訴我們有兩個伺服器彼此衝突。我們需要使用 server.shutdown()
方法來清除上一個測試之後的資源,以及此新測試結束時的資源。
我們可以先開啟 server.js
檔案,並確保我們的 makeServer
函式會回傳伺服器實例來做到這一點
// server.js
export default function (environment = "development") {
return createServer({
// rest of server
})
}
然後回到我們的測試中,我們可以將 makeServer
的回傳值賦予給一個本地變數,並使用它在每個測試結束時呼叫 server.shutdown()
test("it shows a message when there are no reminders", async () => {
let server = makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
server.shutdown()
})
test("it shows existing reminders", async () => {
let server = makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
server.shutdown()
})
現在兩個測試都應該執行並通過,而且不再有 Pretender 的警告。
現在讓我們來實際撰寫第二個測試。
由於我們想要測試當伺服器一開始有三個提醒事項時,UI 的行為如何,我們需要在呼叫 visit('/')
之前建立這些資料。
我們可以像在 seeds()
hook 中所做的那樣,使用新的本地 server
變數,直接在我們的測試中植入伺服器。事實上,讓我們複製這三個 server.create
陳述式並將它們帶到我們的測試中
test("it shows existing reminders", async () => {
let server = makeServer("test")
server.create("reminder", { text: "Walk the dog" })
server.create("reminder", { text: "Take out the trash" })
server.create("reminder", { text: "Work out" })
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
您應該會看到測試失敗
找不到具有文字的元素:「全部完成!」
如果您查看除錯輸出,應該會在 HTML 中看到我們的三個提醒事項
這正是我們想要的!
讓我們更新我們的斷言
test("it shows existing reminders", async () => {
let server = makeServer("test")
server.create("reminder", { text: "Walk the dog" })
server.create("reminder", { text: "Take out the trash" })
server.create("reminder", { text: "Work out" })
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("Walk the dog")).toBeInTheDocument()
expect(screen.getByText("Take out the trash")).toBeInTheDocument()
expect(screen.getByText("Work out")).toBeInTheDocument()
server.shutdown()
})
這樣一來,我們的兩個測試現在都通過了!
如您所見,每個測試都為我們提供了一個獨立的地方,可以為了我們正在測試的任何情境而變更 Mirage 伺服器的狀態。由於我們在每個測試後都會進行清理,因此這些變更都不會從一個測試洩漏到另一個測試中。
讓我們快速重構一下。由於每個測試都會啟動和停止我們的基本 Mirage 伺服器,我們可以利用 Jest 的 beforeEach
和 afterEach
hook 來清理這些程式碼
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"
let server
beforeEach(() => {
server = makeServer("test")
})
afterEach(() => {
server.shutdown()
})
test("it shows a message when there are no reminders", async () => {
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
test("it shows existing reminders", async () => {
server.create("reminder", { text: "Walk the dog" })
server.create("reminder", { text: "Take out the trash" })
server.create("reminder", { text: "Work out" })
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("Walk the dog")).toBeInTheDocument()
expect(screen.getByText("Take out the trash")).toBeInTheDocument()
expect(screen.getByText("Work out")).toBeInTheDocument()
})
儲存後,兩個測試應該會再次通過。
此變更確保我們的伺服器始終被清理乾淨,並且也讓我們可以撰寫專注於與我們正在驗證的真實使用者故事相關的較高層級步驟的測試:給定伺服器上存在三個提醒事項,當使用者訪問應用程式時,那麼他們會希望在頁面上看到它們。
讓我們再撰寫一個測試。我們將測試我們可以為特定清單建立新的提醒事項。
我們將從使用清單植入 Mirage 伺服器開始我們的測試,然後我們將訪問該清單的 URL
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
})
請注意在測試中使用來自 Mirage 的資料有多實用。如果您想了解此時的渲染輸出,可以使用 ?open
查詢參數來確保應用程式的側邊欄已開啟,並呼叫 screen.debug()
以查看輸出中的清單
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}?open`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
screen.debug()
})
您應該會在側邊欄 UI 中看到「全部」和「清單 0」
現在,如果我們從此 URL 建立新的提醒事項,它應該會與此清單相關聯,就像我們在第 7 部分中在開發期間所做的那樣。
讓我們逐步進行。首先,將 userEvent
匯入到檔案的頂部
import userEvent from "@testing-library/user-event"
然後使用它來點擊並輸入適當的元素。我們使用 data-testid
屬性來識別它們。
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
userEvent.click(screen.getByTestId("add-reminder"))
await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
userEvent.click(screen.getByTestId("save-new-reminder"))
// assert something
})
現在,點擊提交按鈕後我們應該做什麼?
如果您切換回開發環境並嘗試建立提醒事項,您會看到在建立提醒事項後,文字方塊會淡出然後隱藏。
因此,在我們的測試中,我們可以等待輸入消失,然後斷言新的提醒事項出現在清單中
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
userEvent.click(screen.getByTestId("add-reminder"))
await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
userEvent.click(screen.getByTestId("save-new-reminder"))
await waitForElementToBeRemoved(() => screen.getByTestId("new-reminder-text"))
expect(screen.getByText("Work out")).toBeInTheDocument()
})
而且它運作了!
作為最後一步,針對 Mirage 伺服器的狀態進行斷言通常是有意義的,這可以讓您對前端程式碼是否按您的想法運作更有信心。
在我們的案例中,如果一切運作正常,我們應該在 Mirage 的資料庫中建立一個新的提醒事項,並且它應該與我們建立的清單相關聯。我們可以很容易地將這些斷言與我們的 UI 斷言一起加入
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
userEvent.click(screen.getByTestId("add-reminder"))
await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
userEvent.click(screen.getByTestId("save-new-reminder"))
await waitForElementToBeRemoved(() => screen.getByTestId("new-reminder-text"))
expect(screen.getByText("Work out")).toBeInTheDocument()
expect(server.db.reminders.length).toEqual(1)
expect(server.db.reminders[0].listId).toEqual(list.id)
})
您可以將此視為一個簡單的方法,可以驗證您的 UI 是否透過網路傳送正確的 JSON 酬載,而無需降級到針對 HTTP 請求和回應資料進行斷言的較低層級。
呼~這是本教學中最長的一步,但您完成了許多工作!您已經有了四個測試涵蓋應用程式的一些重要功能,並且您能夠重複使用 Mirage 伺服器,只對每個測試進行所需的變更。
希望您能看到,當您能夠利用現有的 Mirage 伺服器,並且只調整必要的內容,讓您的伺服器處於每個測試的特定狀態時,撰寫測試會有多愉快。
重點整理
- Mirage 讓您可以在開發和測試之間輕鬆共享您的模擬伺服器
- 測試時,請為您的 Mirage 伺服器使用
test
環境,這樣您的測試執行速度會很快,而且資料庫一開始會是空的 - 請善用您可以在測試中輕鬆存取 Mirage 伺服器的事實,來執行諸如訪問動態 URL 或針對伺服器資料庫所做的變更進行斷言之類的操作