ファクトリ

Mirage を使用する主な利点の 1 つは、サーバーを迅速にさまざまな状態に設定できることです。

たとえば、機能を開発していて、ログインユーザーと匿名ユーザーの両方について UI がどのようにレンダリングされるかを確認したい場合があります。これは、実際のバックエンドサーバーを使用する場合に問題となる種類の処理ですが、Mirage を使用すると、JavaScript 変数を切り替えてアプリのライブリロードを確認するだけで済みます。

ファクトリは、データ作成ロジックを整理するのに役立つクラスであり、開発中またはテスト中にさまざまなサーバー状態を簡単に定義できます。

動作を確認してみましょう。

ファクトリの定義

最初のファクトリ

Mirage で定義されたMovieモデルがあるとします。

import { createServer, Model } from "miragejs"

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

アプリの開発を開始するために、Mirage のデータベースにいくつかの映画をシードするには、サーバーのseedsserver.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の2番目の引数として渡すことができます。

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メソッドを使用すると、5つの映画をすばやく生成できます。

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"
    }
  ]
}

ご覧のとおり、ファクトリを使用すると、動的なサーバーデータに対してさまざまなシナリオを迅速に生成できます。

属性の上書き

ファクトリはモデルの「基本ケース」を定義するのに最適ですが、ファクトリの属性を特定の値で上書きしたい場合があります。

create および createList への最後の引数は、ファクトリからの属性を上書きする属性の 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" }
]

リレーションシップ

基盤となる schema オブジェクトを使用して ORM でリレーショナルデータを作成するのと同じように

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 })
  })
})

このコードは、それぞれに 10 件の投稿があり、各投稿に 5 件のコメントがある 5 人のユーザーを生成します。これらのリレーションシップがモデルで定義されていると仮定すると、すべての外部キーは Mirage のデータベースに正しく設定されます。

afterCreateフック

多くの場合、(前のセクションで示したように)手動でリレーションシップを設定することで十分です。ただし、基本ケースのリレーションシップを自動的に設定する方が理にかなう場合があります。

ここで afterCreate フックが役立ちます。これは、ファクトリの基本属性を使用してモデルが作成された後に呼び出されるフックです。このフックを使用すると、新しく作成されたモデルが createcreateList から返される前に、追加のロジックを実行できます。

動作を確認してみましょう。

アプリにこれらの2つのモデルがあるとします。

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)であり、2 番目の引数は Mirage サーバーインスタンスへの参照です。これにより、他のファクトリを呼び出したり、新しく作成されたオブジェクトをカスタマイズするために必要な他のサーバーの状態を検査したりできます。

この例では、ファクトリはすぐにこの投稿のユーザーを作成します。つまり、アプリの他の場所(テストなど)では、投稿を作成するだけで済みます。

server.create("post")

その投稿には自動的に関連付けられたユーザーが作成され、関連付けられるため、有効なレコードを操作できます。

さて、これまでに実装してきた方法には1つの問題があります。私たちの 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 レコードが得られます。

afterCreatehasMany 関連付けの作成にも使用でき、その他の関連する作成ロジックを適用することもできます。

トレイト

トレイトは、関連する属性を簡単にグループ化できるファクトリの重要な機能です。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 に渡すことができます。

create または createList にトレイトの名前を文字列引数として渡すことで、新しいトレイトを使用できます。

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

作成された投稿には、基本属性と published トレイトの下にあるすべての属性が含まれます。

複数のトレイトを組み合わせて使用することもできます。2 つのトレイトが定義されている次のファクトリがあるとします。

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

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

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

新しいトレイトを create または createList に任意の順序で渡すことができます。

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

複数のトレイトが同じ属性を設定する場合、最後のトレイトが優先されます。

常に、トレイトを使用している場合でも、属性の上書きオブジェクトを最後の引数として渡すことができます。

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

afterCreate() フックと組み合わせることで、トレイトは関連オブジェクトグラフの設定プロセスを簡素化します。

ここでは、新しく作成された投稿に3つのコメントを作成する withComments トレイトを定義します。

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

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

このトレイトを使用して、3つのコメントを含む10個の投稿を迅速に作成できます。

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

afterCreate フックとトレイトを組み合わせることは、Mirage ファクトリの最も強力な機能の1つです。このテクニックを効果的に使用することで、アプリのリレーショナルデータのさまざまなグラフを作成するプロセスが大幅に簡素化されます。

1つ以上のトレイトを使用してオブジェクトを作成する場合、ファクトリは適用可能なすべての afterCreate フックを実行します。基本ファクトリの afterCreate フックが最初に実行され(存在する場合)、トレイトフックは create または createList への呼び出しでトレイトが指定された順序で実行されます。

関連付けヘルパー

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 はサーバーの設定をすべて読み込みますが、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アプリを起動すると、映画が見つからなかったというメッセージを含む要素がドキュメントに存在することをアサートします。

2番目のテストでも、Mirageサーバーを起動しますが、今回は5本の映画をシードします。今度はReactアプリをレンダリングすると、movie-list要素が存在するのを待ちます。awaitを使用するのは、Reactアプリが非同期であるネットワークリクエストを行っているためです。Mirageがそのリクエストに応答したら、それらの映画がUIに表示されることをアサートします。

各テストは新しいMirageサーバーから開始されるため、Mirageの状態はテスト間でリークしません。

Mirageを使ったテストの詳細については、これらのガイドの「テスト」セクションをご覧ください。

ファクトリのベストプラクティス

一般的に、モデルの基本ファクトリは、そのモデルの最小限の有効な状態を構成する属性とリレーションシップのみを使用して定義するのが最適です。その後、afterCreateとトレイトを使用して、基本ケースの上に有効で関連する変更を含む他の一般的な状態を定義できます。

このアドバイスは、テストスイートを維持可能な状態に保つ上で非常に役立ちます。

トレイトと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は、セッションとユーザーを有効な状態で設定し、ユーザーを投稿に関連付けることができます。そのため、テストは簡潔で、実際にテストしている内容に集中できます。

トレイトとafterCreateを効果的に使用することで、テストスイートはより堅牢になり、データレイヤーの変更に対してより耐性を持つようになります。なぜなら、テストはアサーションを確認するために必要な最小限の設定ロジックのみを宣言するからです。


次に、データベースのシード方法の代替手段として、Fixtureの使用方法を見ていきます。