工廠

使用 Mirage 的主要好處之一是能夠快速將伺服器設定為不同的狀態。

例如,您可能正在開發一個功能,並希望查看已登入使用者和匿名使用者的 UI 呈現方式。當使用真實的後端伺服器時,這種事情很麻煩,但使用 Mirage,它就像翻轉一個 JavaScript 變數並觀察您的應用程式即時重新載入一樣簡單。

工廠是類別,可協助您組織資料建立邏輯,以便在開發期間或測試中更輕鬆地定義不同的伺服器狀態。

讓我們看看它們如何運作。

定義工廠

您的第一個工廠

假設我們在 Mirage 中定義了一個 Movie 模型。

import { createServer, Model } from "miragejs"

createServer({
  models: {
    movie: Model,
  },
})

要使用一些電影來填充 Mirage 的資料庫,以便您可以開始開發應用程式,請在伺服器的 seeds 中使用 server.create 方法

import { createServer, Model } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  seeds(server) {
    server.create("movie")
  },
})

server.create 將模型的類別名稱的單數連字號形式作為其第一個引數。

因為我們沒有為 Movie 定義工廠,server.create('movie') 只會建立一個空記錄並將其插入資料庫中

// server.db.dump();
{
  movies: [{ id: "1" }]
}

不是很有趣的記錄。

但是,我們可以將我們自己的屬性作為第二個引數傳遞給 server.create

server.create("movie", {
  title: "Interstellar",
  releaseDate: "10/26/2014",
  genre: "Sci-Fi",
})

現在我們的資料庫看起來像這樣

// server.db.dump()

{
  "movies": [
    {
      "id": "1",
      "title": "Interstellar",
      "releaseDate": "10/26/2014",
      "genre": "Sci-Fi"
    }
  ]
}

我們實際上可以開始針對真實資料開發我們的 UI。

這是一個很好的開始方式,但在處理資料驅動的應用程式時,手動定義每個屬性(和關係)可能會很麻煩。如果我們有一種可以動態產生其中一些屬性的方法就好了。

幸運的是,工廠可以讓我們做到這一點!

讓我們使用伺服器選項的 factories 金鑰和 Factory 匯入,為我們的 Movie 模型定義一個工廠

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      // factory properties go here
    }),
  },

  seeds(server) {
    server.create("movie")
  },
})

現在工廠是空的。讓我們在其上定義一個屬性

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      title: "Movie title",
    }),
  },

  seeds(server) {
    server.create("movie")
  },
})

現在 server.create('movie') 將使用此工廠的屬性。插入的記錄將如下所示

{
  "movies": [{ "id": "1", "title": "Movie title" }]
}

我們也可以將此屬性設為函式。

Factory.extend({
  title(i) {
    return `Movie ${i}`
  },
})

i 是一個遞增的索引,可讓我們建立動態工廠屬性。

如果我們使用 server.createList 方法,我們可以快速產生五部電影

server.createList("movie", 5)

有了上述的工廠定義,我們的資料庫現在會像這樣:

{
  "movies": [
    { "id": "1", "title": "Movie 1" },
    { "id": "2", "title": "Movie 2" },
    { "id": "3", "title": "Movie 3" },
    { "id": "4", "title": "Movie 4" },
    { "id": "5", "title": "Movie 5" }
  ]
}

讓我們在工廠中加入更多屬性。

import { createServer, Model, Factory } from "miragejs"
import faker from "faker"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      title(i) {
        return `Movie ${i}`
      },

      releaseDate() {
        return faker.date.past().toLocaleDateString()
      },

      genre(i) {
        let genres = ["Sci-Fi", "Drama", "Comedy"]

        return genres[i % genres.length]
      },
    }),
  },

  seeds(server) {
    // Use factories here
  },
})

這裡我們安裝了 Faker.js 函式庫,以協助我們產生隨機日期。

現在,如果我們在開發種子中建立 5 部電影:

seeds(server) {
  server.createList('movie', 5)
}

我們的資料庫中會有這些資料:

{
  "movies": [
    {
      "id": "1",
      "title": "Movie 1",
      "releaseDate": "5/14/2018",
      "genre": "Sci-Fi"
    },
    {
      "id": "2",
      "title": "Movie 2",
      "releaseDate": "2/22/2019",
      "genre": "Drama"
    },
    {
      "id": "3",
      "title": "Movie 3",
      "releaseDate": "6/2/2018",
      "genre": "Comedy"
    },
    {
      "id": "4",
      "title": "Movie 4",
      "releaseDate": "7/29/2018",
      "genre": "Sci-Fi"
    },
    {
      "id": "5",
      "title": "Movie 5",
      "releaseDate": "6/30/2018",
      "genre": "Drama"
    }
  ]
}

如您所見,工廠讓我們可以快速產生不同的動態伺服器資料情境。

屬性覆寫

工廠非常適合定義模型的「基本情況」,但在許多情況下,您會希望使用特定值覆寫工廠的屬性。

createcreateList 的最後一個參數接受一個 POJO 屬性,該屬性將覆寫工廠中的任何內容。

// Using only the base factory
server.create('movie');
// gives us this object:
{ id: '1', title: 'Movie 1', releaseDate: '01/01/2000' }

// Passing in specific values to override certain attributes
server.create('movie', { title: 'Interstellar' });
// gives us this object:
{ id: '2', title: 'Interstellar', releaseDate: '01/01/2000' }

將您的工廠屬性視為模型的合理「基本情況」,然後在開發和測試情境中,根據您對特定值的需求覆寫它們。

依賴屬性

屬性可以透過函數內的 this 來依賴其他屬性。這對於從名稱快速產生使用者名稱等項目非常有用。

factories: {
  user: Factory.extend({
    name() {
      return faker.name.findName()
    },

    username() {
      return this.name.replace(" ", "").toLowerCase()
    },
  })
}

使用此工廠呼叫 server.createList('user', 3) 將會產生這些資料:

[
  { "id": "1", "name": "Retha Donnelly", "username": "rethadonnelly" },
  { "id": "2", "name": "Crystal Schaefer", "username": "crystalschaefer" },
  { "id": "3", "name": "Jerome Schoen", "username": "jeromeschoen" }
]

關係

如同您使用 ORM 與底層的 schema 物件建立關聯式資料的方式相同。

let nolan = schema.people.create({ name: "Christopher Nolan" })

schema.movies.create({
  director: nolan,
  title: "Interstellar",
})

您也可以使用工廠建立關聯式資料。

let nolan = server.create("director", { name: "Christopher Nolan" })

server.create("movie", {
  director: nolan,
  title: "Interstellar",
})

nolan 是一個模型實例,這就是為什麼我們可以在建立星際效應電影時將其作為屬性覆寫傳入。

這在使用 createList 時也有效。

server.create("actor", {
  movies: server.createList("movie", 3),
})

透過這種方式,您可以使用工廠來協助您快速建立關聯式資料圖。

server.createList("user", 5).forEach((user) => {
  server.createList("post", 10, { user }).forEach((post) => {
    server.createList("comment", 5, { post })
  })
})

此程式碼會產生 5 位使用者,每位使用者有 10 則貼文,每則貼文有 5 則留言。假設這些關係已在您的模型中定義,則所有外鍵都會在 Mirage 的資料庫中正確設定。

afterCreate Hook

在許多情況下,手動設定關係(如上一節所示)是完全可以的。但是,有時自動為您設定基本情況關係更有意義。

這就是 afterCreate Hook 派上用場的地方。它是一個在模型使用工廠的基本屬性建立後呼叫的 Hook。此 Hook 可讓您在從 createcreateList 傳回新建立的模型之前,對其執行額外的邏輯。

讓我們看看它是如何運作的。

假設您的應用程式中有這兩個模型:

import { createServer, Model, belongsTo } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },
})

讓我們進一步假設,在您的應用程式中,建立沒有關聯使用者的貼文永遠無效。

您可以使用 afterCreate 來強制執行此行為。

import { createServer, Model, belongsTo, Factory } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },

  factories: {
    post: Factory.extend({
      afterCreate(post, server) {
        post.update({
          user: server.create("user"),
        })
      },
    }),
  },
})

afterCreate 的第一個引數是剛剛建立的物件(在此範例中為 post),第二個引數是對 Mirage 伺服器實例的參考,以便您可以叫用其他工廠或檢查自訂新建立的物件所需的任何其他伺服器狀態。

在此範例中,我們的工廠會立即為此貼文建立使用者。這表示在您的應用程式的其他地方(例如,測試中),您可以只建立貼文:

server.create("post")

您將使用有效的記錄,因為該貼文會自動建立並與關聯的使用者關聯。

現在,我們到目前為止的實作方式有一個問題。我們的 afterCreate Hook 會更新貼文的使用者,無論該貼文是否已關聯使用者

這表示此程式碼:

let jane = server.create("user", { name: "Jane" })
server.createList("post", 10, { user: jane })

將無法如我們預期地運作,因為屬性覆寫是在建立物件時使用,但 afterCreate 中的邏輯會在貼文建立執行。因此,此貼文將會與 Hook 中新建立的使用者關聯,而不是 Jane。

為了修正此問題,我們可以更新 afterCreate Hook,以先檢查新建立的貼文是否已關聯使用者,只有在沒有關聯使用者時,我們才會建立新的使用者並更新關係。

Factory.extend({
  afterCreate(post, server) {
    if (!post.user) {
      post.update({
        user: server.create("user"),
      })
    }
  },
})

現在,呼叫者可以傳入特定使用者:

server.createList("post", 10, { user: jane })

如果該使用者的詳細資訊並不重要,也可以省略指定使用者:

server.create("post")

在這兩種情況下,他們最終都會獲得有效的 Post 記錄。

afterCreate 也可用於建立 hasMany 關聯,以及套用任何其他相關的建立邏輯。

特性

特性是工廠的一個重要功能,可讓您輕鬆地將相關屬性分組。透過匯入 trait 並將新鍵新增至您的工廠來定義它們。

例如,這裡我們在我們的貼文工廠中定義一個名為 published 的特性。

import { createServer, Model, Factory, trait } from "miragejs"

createServer({
  models: {
    post: Model,
  },

  factories: {
    post: Factory.extend({
      title: "Lorem ipsum",

      published: trait({
        isPublished: true,
        publishedAt: "2010-01-01 10:00:00",
      }),
    }),
  },
})

您可以將任何可以放入基本工廠的內容傳入 trait 中。

我們可以透過將特性的名稱作為字串引數傳遞至 createcreateList 來使用我們的新特性。

server.create("post", "published")
server.createList("post", 3, "published")

建立的貼文將會具有所有基本屬性,以及 published 特性下的所有內容。

您也可以將多個特性組合在一起。假設有以下工廠定義了兩個特性:

post: Factory.extend({
  title: "Lorem ipsum",

  published: trait({
    isPublished: true,
    publishedAt: "2010-01-01 10:00:00",
  }),

  official: trait({
    isOfficial: true,
  }),
})

我們可以以任何順序將我們的新特性傳遞至 createcreateList

let officialPost = server.create("post", "official")
let officialPublishedPost = server.create("post", "official", "published")

如果多個特性設定相同的屬性,則最後一個特性勝出。

一如既往,您可以傳入一個屬性覆寫的物件作為最後一個引數,即使您正在使用特性也是如此。

server.create("post", "published", { title: "My first post" })

當與 afterCreate() Hook 結合使用時,特性可以簡化設定相關物件圖的程序。

這裡我們定義一個 withComments 特性,它會為新建立的貼文建立 3 則留言。

post: Factory.extend({
  title: "Lorem ipsum",

  withComments: trait({
    afterCreate(post, server) {
      server.createList("comment", 3, { post })
    },
  }),
})

我們可以使用此特性快速建立 10 則貼文,每則貼文有 3 則留言。

server.createList("post", 10, "withComments")

將特性與 afterCreate Hook 結合使用是 Mirage 工廠最強大的功能之一。有效地使用這項技術將會大幅簡化為您的應用程式建立不同的關聯式資料圖的程序。

當建立具有一個或多個特性的物件時,工廠將會執行每個適用的 afterCreate Hook。基本工廠的 afterCreate Hook 會先執行(如果存在),然後任何特性 Hook 會依特性在呼叫 createcreateList 時指定的順序執行。

關聯 Helper

association() Helper 提供一些糖分來建立 belongsTo 關係。

如我們稍早所見,afterCreate Hook 讓我們可以預先連結關係。

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },

  factories: {
    post: Factory.extend({
      afterCreate(post, server) {
        if (!post.user) {
          post.update({
            user: server("user"),
          })
        }
      },
    }),
  },
})

association() Helper 有效地取代了此程式碼:

import { createServer, Model, Factory, association } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },

  factories: {
    post: Factory.extend({
      user: association(),
    }),
  },
})

這應該有助於減少工廠定義中的一些重複程式碼。

您也可以在特性中使用 association()。此定義:

post: Factory.extend({
  withUser: trait({
    user: association(),
  }),
})

可讓您撰寫 server.create('post', 'withUser') 來建立具有關聯使用者的貼文。

您也可以將其他特性和覆寫傳遞至相關模型工廠的 association()

post: Factory.extend({
  withUser: trait({
    user: association("admin", { role: "editor" }),
  }),
})

請注意,如果您的 belongsTo 關係是多型的,則無法使用 association() Helper。此外,association() 不適用於 hasMany 關係。在這兩種情況下,您都應該繼續使用 afterCreate Hook 來植入您的資料。

使用工廠

在開發中

若要使用你的工廠函數來初始化你的開發資料庫,請在伺服器的 seeds 函數中呼叫 server.createserver.createList

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      title(i) {
        return `Movie ${i}`
      },
    }),
  },

  seeds(server) {
    server.createList("movie", 10)
  },
})

開發環境中沒有明確的 API 可以切換情境,但你可以使用 JavaScript 模組來區分不同的設定。

舉例來說,你可以為每個情境建立一個新檔案,其中包含一些初始化邏輯。

// mirage/scenarios/admin.js
export default function (server) {
  server.create("user", { isAdmin: true })
}

...從一個 index.js 檔案中匯出所有情境作為一個物件。

// mirage/scenarios/index.js
import anonymous from "./anonymous"
import subscriber from "./subscriber"
import admin from "./admin"

export default scenarios = {
  anonymous,
  subscriber,
  admin,
}

...然後將該物件匯入到 default.js

現在,你可以透過更改單一變數來快速切換開發狀態。

// mirage/server.js
import scenarios from "./scenarios"

// Choose one
const state =
  // 'anonymous'
  // 'subscriber'
  "admin"

createServer({
  // other config,

  seeds: scenarios[state],
})

這在開發你的應用程式或與團隊分享新功能的不同狀態時非常方便。

在測試中

當你在 test 環境中運行伺服器時,你的伺服器行為會略有不同。

createServer({
  environment: "test", // default is development

  seeds(server) {
    // This function is ignored when environment is "test"
    server.createList("movie", 10)
  },
})

test 環境中,Mirage 會載入你所有的伺服器設定,但會忽略你的 seeds。(它還會將路由處理器的 timing 設定為 0,並隱藏主控台的日誌。)

這表示每個測試都從一個乾淨的資料庫開始,讓你有機會只設定該測試所需的狀態。它還可以讓你的開發環境與測試隔離,這樣你在調整 seeds 時就不會不小心破壞你的測試套件。

若要在測試中初始化 Mirage 的資料庫,請使用 server.createserver.createList 方法。

舉例來說,如果你使用 @testing-library/react,你的測試可能會如下所示:

let server

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

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

test("I see a message if there are no movies", () => {
  const { getByTestId } = render(<App />)
  expect(getByTestId("no-movies")).toBeInTheDocument()
})

test("I see a list of the movies from the server", async () => {
  server.createList("movie", 5)

  const { getByTestId } = render(<App />)
  await waitForElement(() => getByTestId("movie-list"))

  expect(getByTestId("movie")).toHaveLength(5)
})

在第一個測試中,我們啟動了 Mirage 伺服器,但沒有使用任何電影資料初始化它。當我們啟動 React 應用程式時,我們斷言文件中存在一個元素,顯示沒有找到任何電影的訊息。

在第二個測試中,我們也啟動了 Mirage 伺服器,但我們使用 5 部電影來初始化它。這次當我們渲染 React 應用程式時,我們會等待一個 movie-list 元素出現。我們使用 await,因為我們的 React 應用程式正在發出網路請求,這是非同步的。一旦 Mirage 回應了該請求,我們就會斷言這些電影會顯示在我們的 UI 中。

每個測試都從一個全新的 Mirage 伺服器開始,因此 Mirage 的狀態不會跨測試洩漏。

你可以在這些指南的「測試」章節中閱讀更多關於使用 Mirage 進行測試的資訊。

工廠最佳實務

一般來說,最好只使用構成該模型最小有效狀態的屬性和關係來定義模型的基本工廠函數。然後,你可以使用 afterCreate 和 traits 來定義其他常見的狀態,這些狀態在基本情況之上包含有效的相關變更。

這個建議對於保持你的測試套件的可維護性有很大的幫助。

如果你不使用 traits 和 afterCreate,你的測試會因與設定該測試所需資料相關的不必要細節而變得遲鈍。

test("I can see the title of a post", async function (assert) {
  let session = server.create("session")
  let user = server.create("user", { session })
  server.create("post", {
    user,
    title: "My first post",
    slug: "my-first-post",
  })

  await visit("/post/my-first-post")

  assert.dom("h1").hasText("My first post")
})

此測試僅關心斷言文章的標題是否渲染到螢幕上,但它有很多樣板程式碼,只是為了使文章處於有效狀態。

如果我們改用 afterCreate,編寫此測試的開發人員可以簡單地建立一個具有指定 titleslug 的文章,因為這些是與測試相關的唯一細節。

test("I can see the title of a post", async function (assert) {
  server.create("post", {
    title: "My first post",
    slug: "my-first-post",
  })

  await visit("/post/my-first-post")

  assert.dom("h1").hasText("My first post")
})

afterCreate 可以負責設定有效的 session 和 user 狀態,並將 user 與文章關聯,以便測試可以保持簡潔並專注於它實際測試的內容。

有效地使用 traits 和 afterCreate 可以使你的測試套件不易崩潰,並且對於資料層的變更更強健,因為測試只聲明驗證其斷言所需的最少設定邏輯。


接下來,我們將看看如何使用 Fixtures 作為初始化資料庫的替代方法。