工厂

使用 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 钩子

在许多情况下,手动设置关系(如上一节所示)是完全可以的。但是,有时为你的关系自动设置基本情况更有意义。

这就是 afterCreate 钩子派上用场的地方。它是在使用工厂的基本属性创建模型后调用的钩子。此钩子允许你在从 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 钩子会更新帖子的用户,无论该帖子是否已与用户关联

这意味着这段代码。

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

将无法按预期工作,因为属性覆盖是在创建对象时使用的,但 afterCreate 中的逻辑帖子创建后运行。因此,该帖子将与来自钩子的新创建用户关联,而不是与 Jane 关联。

为了解决这个问题,我们可以更新我们的 afterCreate 钩子,首先检查新创建的帖子是否已经与用户关联,只有在没有关联的情况下,我们才会创建一个新的用户并更新关系。

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() 钩子结合使用,特征简化了设置相关对象图的过程。

这里我们定义了一个 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 钩子结合使用是 Mirage 工厂最强大的功能之一。有效使用此技术将极大地简化创建应用程序不同关系数据图的过程。

当使用一个或多个特征创建对象时,工厂将运行所有适用的 afterCreate 钩子。基本工厂的 afterCreate 钩子将首先运行(如果存在),并且任何特征钩子都将按特征在对 createcreateList 的调用中指定的顺序运行。

关联助手

association() 助手提供了一些创建 belongsTo 关系的语法糖。

正如我们之前看到的,afterCreate 钩子允许我们预先连接关系。

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() 助手实际上替换了这段代码。

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() 助手。此外,association() 不适用于 hasMany 关系。在这两种情况下,你都应该继续使用 afterCreate 钩子来播种数据。

使用工厂

在开发中

要使用你的工厂来播种开发数据库,请在服务器的 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 将加载所有服务器配置,但它会忽略你的种子。(它还会将 timing 设置为路由处理程序的 0 并隐藏控制台中的日志。)

这意味着每个测试都从一个干净的数据库开始,让你有机会只设置该测试所需的狀態。它还将你的开发环境与你的测试隔离,这样你就可以在调整种子时不会无意中破坏你的测试套件。

要在测试中播种 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 可以负责将会话和用户设置为有效状态,并将用户与帖子关联起来,这样测试就可以保持简洁,专注于它实际测试的内容。

有效地使用 traits 和 afterCreate 使您的测试套件更不易受数据层更改的影响,更健壮,因为测试只声明验证其断言所需的最小设置逻辑。


接下来,我们将看看如何使用 Fixtures 作为一种替代方法来填充您的数据库。