應用程式測試

Mirage 最棒的功能之一,就是它讓 JavaScript 應用程式開發人員能夠編寫高階 UI 測試,而無需與其生產 API 伺服器互動。

這些測試著重於驗證應用程式的使用者流程。此處的使用者是指實際會在電腦或行動裝置上使用您的 Web 應用程式,並透過鍵盤、滑鼠或其他輸入裝置來操作的人。因此,這些測試應與這些人在真實世界中與您的應用程式互動的方式非常相似。

讓我們考慮一個範例。假設您想要測試以下的使用者流程

使用者在瀏覽首頁時可以檢視電影清單

大多數類似的應用程式測試實際上都依賴於給定的伺服器狀態,即使它們省略了或隱含了它。而這就是 Mirage 的用武之地:它可協助您將該伺服器狀態設定為測試中的明確部分。

如果我們要改寫上述測試使其更完整,可能會得到如下結果

  • 假設伺服器上存在 10 個電影資源
  • 使用者瀏覽首頁
  • 然後他們應該看到 10 部電影的清單

讓我們看看如何使用 Mirage 來編寫此測試。

我們的第一個測試

此範例使用 Cypress 作為語法,但 Mirage 可與您設定的任何 JavaScript 測試框架搭配使用。

請參閱我們的 Cypress 快速入門,以瞭解如何在應用程式中使用 Cypress 讓 Mirage 運作。

假設 Cypress 已連線,我們可以編寫此測試

// homepage-test.js
it("shows the movies", () => {
  cy.visit("/")

  cy.get("li.selected").should("have.length", 10)
})

我們的應用程式正在執行,但當它向 /api/movies 發出 HTTP 請求時,會發生錯誤且測試失敗。這是我們可以使用 Mirage 的地方。

讓我們匯入它,並在 beforeEach 中啟動它

// homepage-test.js
import { createServer } from "miragejs"

let server

beforeEach(() => {
  server = createServer()
})

afterEach(() => {
  server.shutdown()
})

it("shows the movies", function () {
  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

當您啟動 Mirage 時,它會攔截您的應用程式的網路請求,就像在開發中一樣。因此,下次您執行測試時,應該會看到類似以下的錯誤

Mirage:您的應用程式嘗試 GET '/api/movies',但未定義任何路由來處理此請求

現在我們可以模擬此路由。

import { createServer, Model } from "miragejs"

let server

beforeEach(() => {
  server = createServer({
    models: {
      movie: Model,
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
})

afterEach(() => {
  server.shutdown()
})

it("shows the movies", function () {
  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

我們的應用程式現在正在執行,但測試失敗。我們可以檢視主控台中的記錄,並看到 Mirage 處理了對 /api/movies 的請求,但它沒有回應任何資料。

那是因為 Mirage 的資料庫是空的。

如同您在指南的先前章節中所學到的,您可以使用 seeds 方法,使用工廠和固定裝置來植入 Mirage 的資料庫。但在測試中,我們已經有一個設定 Mirage 狀態的自然位置 - 測試本身!因此,一般的做法是不要使用種子,而是在每個測試中設定 Mirage 的資料庫狀態。

我們可以透過直接在測試主體中使用 server.createserver.createList 方法來達成

import { createServer, Model } from "miragejs"

let server

beforeEach(() => {
  server = createServer({
    models: {
      movie: Model,
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
})

afterEach(() => {
  server.shutdown()
})

it("shows the movies", function () {
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

現在我們有了一個通過的測試!

在每個測試之後,Mirage 的伺服器都會關閉並重設,因此這些狀態不會跨測試洩漏。

在開發和測試之間共享您的伺服器

在上面的範例中,我們直接在測試中設定了一個新的伺服器,但 Mirage 最好的使用方式是將您的模擬伺服器定義集中化,並在您的開發和測試環境之間共享。畢竟,在生產環境中,您的應用程式將會與一個使用單一 API 合約的真實伺服器進行通訊!因此,使用單一的 Mirage 伺服器有助於您在所有使用的地方維護一個一致的模擬 API 伺服器。

因此,如果您還沒有為開發啟動一個 Mirage 伺服器,請將您的伺服器定義移到一個明確表示它將在開發環境和測試中使用的位置。

└── src
    ├── App.js
    ├── App.test.js
    └── mirage.js

接下來,導出一個可以用來啟動您的 Mirage 伺服器的函式。

// src/mirage.js
import { createServer, Model } from "miragejs"

export function startMirage() {
  return createServer({
    models: {
      movie: Model,
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
}

現在,導入這個函式並在您的測試中使用它。

// App.test.js
import { startMirage } from "./mirage"

describe("homepage", function () {
  let server

  beforeEach(() => {
    server = startMirage()
  })

  afterEach(() => {
    server.shutdown()
  })

  it("shows the movies", function () {
    server.createList("movie", 10)

    cy.visit("/")

    cy.get("li.movie").should("have.length", 10)
  })
})

您現在有一個集中定義和更新 Mirage 伺服器的地方,並且有一個簡單的方法可以在您的測試中使用它。

您也可以使用您的 startMirage 函式在開發中啟動 Mirage。

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import { startMirage } from "./mirage"

if (process.env.NODE_ENV === "development") {
  startMirage()
}

ReactDOM.render(<App />, document.getElementById("root"))

理想情況下,您應該確保您的 Mirage 程式碼不會包含在生產環境中(除非您正在建構原型)。如何做到這一點將取決於您的建置工具設定。未來,我們將會新增一些涵蓋這方面的更多指南。

test 環境

Mirage 有一個 environment 選項,預設為 development。在開發環境中,Mirage 的延遲為 50ms (可自訂),會將所有回應記錄到主控台,並載入開發的 seeds

Mirage 也可以置於 test 環境中,該環境將以 0 延遲開始(以保持您的測試快速),並抑制其所有記錄(以免弄髒您的 CI 伺服器日誌)。它也會忽略 seeds() 函式,以便數據可以僅用於開發,而不會洩漏或影響您的測試。這有助於保持您的測試確定性。

為了在我們的測試中使用 test 環境,讓我們更新我們的 startMirage 函式以接受環境選項,預設為 development

  // src/mirage.js
  import { createServer, Model } from "miragejs"

- export function startMirage() {
+ export function startMirage({ environment: 'development' }) {
    return createServer({
+     environment,

      models: {
        movie: Model,
      },

      routes() {
        this.namespace = "api"

        this.resource("movie")
      },
    })
  }

現在在我們的測試中,我們可以傳入 test 作為環境。我們的測試將以零延遲運行,我們不會得到任何 seeds(),並且我們不會看到任何日誌。

  // src/App.test.js
  import { startMirage } from "./mirage"

  describe("homepage", function() {
    let server

    beforeEach(() => {
-     server = startMirage()
+     server = startMirage({ environment: 'test' })
    })

    afterEach(() => {
      server.shutdown()
    })

    it("shows the movies", function() {
      server.createList("movie", 10)

      cy.visit("/")

      cy.get("li.movie").should("have.length", 10)
    })
  })

如果您發現自己正在除錯測試,並且想要查看進出 Mirage 的網路請求,您可以透過將 server.logging 選項設定為 true,在該測試中啟用記錄。

it("shows the movies", function () {
  server.logging = true // enable logs for this test while debugging
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

完成後請刪除它,以保持您的 CI 日誌清潔。

保持您的測試專注

工廠在保持與測試相關的程式碼盡可能靠近該測試方面非常重要。在上面的範例中,我們想要驗證使用者會看到十部電影,前提是伺服器上存在這些電影。因此,server.createList('movie', 10) 呼叫直接在測試中。

假設我們想要測試當使用者瀏覽一部名為「星際效應」的電影的詳細資訊路由時,他們會在 <h1> 標籤中看到該標題。實現此目的的一種方法是將標題硬式編碼到我們伺服器的 Movie 工廠中。

// src/mirage.js
import { createServer, Model, Factory } from "miragejs"

export function startMirage({ environment: 'development' }) {
  return createServer({
    environment,

    models: {
      movie: Model,
    },

    factories: {
      movie: Factory.extend({
        title: 'Interstellar'
      })
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
}

這種方法的問題在於,我們剛剛對我們共享的 Mirage 伺服器進行了一個非常特定於此單一測試的變更。

假設另一個測試需要驗證具有不同標題的電影的不同內容。更改工廠以適應這種情況將會破壞此測試。

因此,您應該使用 createcreateList 來覆蓋您模型的特定屬性。這將使與您的測試相關的程式碼靠近您的測試,而不會使您的其餘測試套件變得脆弱。

// App.test.js
let server

beforeEach(() => {
  server = startMirage({ environment: "test" })
})

afterEach(() => {
  server.shutdown()
})

it("shows all the movies", function () {
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

it("shows the movie's title on the detail route", function () {
  let movie = this.server.create("movie", {
    title: "Interstellar",
  })

  cy.visit(`/${movie.id}`)

  cy.get("h1").should("contain", "Interstallar")
})

在這兩個測試中,我們在測試中直接建立所有相關資料。

第一個測試呼叫 server.createList('movie', 10) 並且不指定任何屬性覆寫,因為每部電影的具體細節與測試的斷言無關。

第二個測試使用 server.create 和特定標題,因為測試正在驗證標題是否顯示在 UI 中。您還可以發現此測試正在使用 Mirage 的自動生成 ID 來瀏覽特定電影的動態 URL。

肯定會有時候您想要設定一些在多個測試之間共享,或者在開發和測試中都使用的資料。我們將在此頁面的下方進一步討論。

安排、執行、斷言

Mirage 建議使用 安排、執行、斷言方法 來撰寫測試。您有時會聽到這個模式被稱為 AAA 測試(「三 A 測試」)。

您可以在我們上面的測試中看到這種結構。

it("shows all the movies", function () {
  // ARRANGE
  server.createList("movie", 10)

  // ACT
  cy.visit("/")

  // ASSERT
  cy.get("li.movie").should("have.length", 10)
})

當然,有時候打破此規則是合理的(例如,在測試的開頭或中間新增一些額外的斷言),但總體而言,您應該盡力遵循此模式。

測試錯誤

為了測試您的應用程式如何回應伺服器錯誤,您可以直接在測試中覆寫路由處理常式。

import { Response } from "miragejs"

it("shows an error if the save attempt fails", function () {
  server.post("/questions", () => {
    return new Response(500, {}, { errors: ["The database went on vacation"] })
  })

  cy.visit("/")
    .contains("New")
    .click()
    .get("input")
    .type("New question")
    .contains("Save")
    .click()

  cy.get("h2").should("contain", "The database went on vacation")
})

此路由處理常式定義僅在此測試期間有效,因此一旦結束,您在 mirage.js 檔案中為 POST 到 /questions 定義的任何處理常式將再次被使用。

測試中共享的資料種子

當 Mirage 的環境設定為 test 時,會忽略 seeds() 組態選項,因此對其的變更不會影響您的其餘測試套件。

如果有一些邏輯您想要在開發情境和您的測試之間,或在多個測試之間共享,您可以隨時建立一個新的純 JavaScript 模組,並在需要的地方導入它。

為了在開發期間使用共享模組,請建立模組。

// mirage/scenarios/shared.js
export default function(server) {
  server.loadFixtures('countries');

  server.createList('event', 10);
});

...在 seeds() 中載入它,以便情境在開發期間執行。

// mirage.js
import sharedScenario from "./scenarios/shared"

createServer({
  seeds(server) {
    // Load the shared scenario in development
    sharedScenario(server)

    // Make some development-specific data
    server.create("movie", { title: "Interstellar" })
  },
})

...然後也在您的測試中載入它(甚至在通用的測試設定函式中載入)。

import sharedScenario from "./mirage/scenarios/shared"
import { startServer } from "./mirage"

let server

beforeEach(() => {
  server = startServer({ environment: "test" })
  sharedScenario(server)
})

afterEach(() => {
  server.shutdown()
})

it("shows all the movies", function () {
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

這些是使用 Mirage 進行應用程式測試的基本知識!接下來我們來談談整合和單元測試。