概要
Mirageを使用すると、ルートハンドラを記述することでAPIレスポンスをシミュレートできます。
ルートハンドラの最も簡単な例は、オブジェクトを返す関数です。
import { createServer } from "miragejs"
createServer({
routes() {
this.namespace = "api"
this.get("/movies", () => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
})
},
})
これで、アプリが/api/movies
にGETリクエストを行うたびに、Mirageはこのデータで応答します。
APIがアプリとは異なるホストまたはポートにある場合は、urlPrefix
を設定します。
routes() {
this.urlPrefix = 'http://localhost:3000';
このような静的なルートハンドラだけでもかなり遠くまで行けますし、Mirageでの作業に慣れるための良い方法です。すべてのHTTP動詞が機能し、サーバの遅延をシミュレートするために使用できるtiming
オプションがあり、カスタムResponse
を返すことによって、アプリがAPIからエラーを受け取ったときの動作を確認することもできます。
import { createServer, Response } from "miragejs"
createServer({
routes() {
this.namespace = "api"
// Responding to a POST request
this.post("/movies", (schema, request) => {
let attrs = JSON.parse(request.requestBody)
attrs.id = Math.floor(Math.random() * 100)
return { movie: attrs }
})
// Using the `timing` option to slow down the response
this.get(
"/movies",
() => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
},
{ timing: 4000 }
)
// Using the `Response` class to return a 500
this.delete("/movies/1", () => {
let headers = {}
let data = { errors: ["Server did not respond"] }
return new Response(500, headers, data)
})
},
})
動的ルートハンドラ
静的なルートハンドラは機能し、HTTPレスポンスをシミュレートする一般的な方法ですが、上記のようなハードコードされたレスポンスにはいくつかの問題があります。
柔軟性に欠ける。単一のテストのためにルートが応答するデータを変更したい場合はどうすればよいですか? ハンドラ全体を最初から書き直す必要があります。
フォーマットロジックを含む。JSONペイロードの形状に関するロジック(例:上記のペイロードの
movies: []
ルートキー)は、すべてのルートハンドラで複製されます。あまりにも基本的なもの。Mirageサーバでリレーションシップのようなより複雑なものを処理する必要がある場合、これらの単純なアドホックなレスポンスは必ず壊れ始めます。
Mirageには、より強力なサーバ実装を記述するためのデータ層があります。上記の基本的なスタブデータを置き換える方法を見てみましょう。
まず、動的なMovie
モデルがあることをMirageに伝えます。
import { createServer, Model } from "miragejs"
createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.get("/movies", () => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
})
},
})
モデルを使用すると、ルートハンドラはMirageのインメモリデータベースを利用できます。データベースにより、ルートハンドラは動的になり、ハンドラを書き直すことなく返されるデータを変更できます。
ルートハンドラを動的に更新しましょう。
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
schema
引数は、新しいMovie
モデルにアクセスする方法です。このルートは、リクエスト時にMirageのデータベースにあるすべてのデータを返します。したがって、このルートが応答するデータを変更するには、Mirageのデータベースにあるレコードのみを変更すればよいです。
最後のステップはデータベースのシードです。現時点では、上記の新しいハンドラにリクエストを送信した場合、レスポンスは次のようになります。
// GET /api/movies
{
"movies": []
}
これはMirageのデータベースが空であるためです。シードを使用して、いくつかの初期データでデータベースを開始できます。
createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
},
seeds(server) {
server.create("movie", { name: "Inception", year: 2010 })
server.create("movie", { name: "Interstellar", year: 2014 })
server.create("movie", { name: "Dunkirk", year: 2017 })
},
})
server.create
は、モデル名と属性オブジェクトを受け取り、新しいデータをデータベースに挿入します。
これで、JavaScriptアプリが/api/movies
にリクエストを行うと、サーバはこれに応答します。
// GET /api/movies
{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}
Mirageのデータベースが各レコードに自動インクリメントIDを自動的に割り当てていることに注意してください。
レスポンスからハードコードされたデータをすべて排除しました。つまり、アプリがMirageのデータベースのデータを時間とともに変更した場合、このエンドポイントへのレスポンスもそれに応じて変化します。
データベース、モデル、そしてスキーマ
APIが、サーバー定義を大幅に簡素化することをご理解いただけたかと思います。以下は、Movie
リソースに関する5つの標準的なRESTfulルートのセットです。
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
this.get("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id)
})
this.post("/movies", (schema, request) => {
let attrs = JSON.parse(request.requestBody)
return schema.movies.create(attrs)
})
this.patch("/movies/:id", (schema, request) => {
let newAttrs = JSON.parse(request.requestBody)
let id = request.params.id
let movie = schema.movies.find(id)
return movie.update(newAttrs)
})
this.delete("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id).destroy()
})
このMirage定義を配置することで、フロントエンドアプリを完全に構築し、テストできます。すべての動的な機能を完成させ、サーバーが存在できるすべての状態を考慮することができます。コードに満足したら、Mirage定義と同じAPI契約を満たす本番サーバーに対してデプロイする準備が整います。
ショートハンド
Mirageには、従来のAPIエンドポイントに必要なコードを削減するためのショートハンドの概念があります。
例えば、以下のルートハンドラー
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
は、次のように記述できます。
this.get("/movies")
post
、patch
(またはput
)、del
メソッドにもショートハンドがあります。上記で定義したMovie
リソースの完全なリソースフルルートを、ショートハンドを使用して記述したものが以下です。
this.get("/movies")
this.get("/movies/:id")
this.post("/movies")
this.patch("/movies/:id")
this.del("/movies/:id")
ショートハンドを使用すると、サーバー定義を簡潔に記述できるので、できる限り使用してください。新しいルートをモックする際には、常にショートハンドから始め、より詳細な制御が必要になった場合に、拡張された関数ルートハンドラーに切り替えるようにしてください。
ファクトリ
上記の例では、server.create
APIを使用してMirageのデータベースにシードデータを挿入しました。
seeds(server) {
server.create("movie", { name: "Inception", year: 2010 })
server.create("movie", { name: "Interstellar", year: 2014 })
server.create("movie", { name: "Dunkirk", year: 2017 })
}
各レコードのすべての属性を渡せるのは便利ですが、新しいデータベースレコードをより迅速に作成したい場合もあります。そこで役立つのがファクトリです。
ファクトリは、Mirageサーバーに対してリアルなデータを作成しやすいオブジェクトです。モデルの設計図と考えてください。
Movie
モデルのファクトリは、次のように作成できます。
import { createServer, Model, Factory } from "miragejs"
createServer({
models: {
movie: Model,
},
factories: {
movie: Factory.extend({}),
},
})
ファクトリには、ブール値、文字列、数値などの単純な型、または動的なデータを取得する関数をプロパティとして定義できます。
import { createServer, Model, Factory } from "miragejs"
createServer({
models: {
movie: Model,
},
factories: {
movie: Factory.extend({
title(i) {
return `Movie ${i}` // Movie 1, Movie 2, etc.
},
year() {
let min = 1950
let max = 2019
return Math.floor(Math.random() * (max - min + 1)) + min
},
rating: "PG-13",
}),
},
})
server.create
APIを使用すると、Mirageはファクトリを使用して新しいデータの生成に役立ちます。(渡した属性のオーバーライドは尊重されます)。
server.create("movie")
server.create("movie")
server.create("movie", { rating: "R" })
server.db.dump()
/*
Mirage's database now contains
{
movies: [
{
id: 1,
title: "Movie 1",
year: 1992,
rating: "PG-13",
},
{
id: 2,
title: "Movie 2",
year: 2008,
rating: "PG-13",
},
{
id: 3,
title: "Movie 3",
year: 1947,
rating: "R",
}
]
}
*/
一度に多数のレコードを生成するためのserver.createList
APIもあります。
server.create
とserver.createList
の両方を使用して、seeds
関数でファクトリを呼び出すことができます。
import { createServer, Factory } from "miragejs"
createServer({
seeds(server) {
server.createList("movie", 10)
},
})
テスト環境でも同様です。テスト環境では、Mirageはルートを読み込みますが、シードデータは無視するため、テストに必要な状態にデータベースを正確に設定できます。
// app-test.js
import React from "react"
import { render, waitForElement } from "@testing-library/react"
import App from "./App"
import startMirage from "./start-mirage"
let server
beforeEach(() => {
server = startMirage({ environment: "test" })
})
afterEach(() => {
server.shutdown()
})
it("shows the list of movies", async () => {
server.createList("movie", 5)
const { getByTestId } = render(<App />)
await waitForElement(() => getByTestId("movie-list"))
expect(getByTestId("movie-item")).toHaveLength(5)
})
ファクトリを使用すると、開発時とテストごとに、Mirageサーバーの初期データを簡単に設定できます。
リレーションシップ
リレーションシップの処理は常に難しく、リレーションシップを扱うエンドポイントのモックも例外ではありません。幸いにも、Mirageにはルートハンドラーをクリーンに保つためのORMが付属しています。
Movie
が複数のCastMembers
を持つとしましょう。このリレーションシップはモデルで宣言できます。
import { createServer, hasMany, belongsTo } from "miragejs"
createServer({
models: {
movie: Model.extend({
castMembers: hasMany(),
}),
castMember: Model.extend({
movie: belongsTo(),
}),
},
})
これで、Mirageはこれら2つのモデル間のリレーションシップを認識し、ルートハンドラーの作成に役立ちます。
this.get("/movies/:id/cast-members", (schema, request) => {
let movie = schema.movies.find(request.params.id)
return movie.castMembers
})
関連データのグラフを作成する場合にも役立ちます。
it("shows the cast members for a movie", async () => {
const movie = server.create("movie", {
title: "Interstellar",
castMembers: [
server.create("cast-member", { name: "Matthew McConaughey" }),
server.create("cast-member", { name: "Anne Hathaway" }),
server.create("cast-member", { name: "Jessica Chastain" }),
],
})
const { getByTestId } = render(<App path={`/movies/${movie.id}`} />)
await waitForElement(() => getByTestId("cast-member-list"))
expect(getByTestId("cast-member")).toHaveLength(3)
})
Mirageは外部キーを使用してこれらの関連モデルを追跡するため、JavaScriptアプリがデータベースへの新しいリレーションシップを読み書きする際に、面倒な帳簿処理の詳細を気にする必要はありません。
シリアライザ
Mirageは、本番APIサーバーを完全に再現できるように設計されています。
これまでのところ、Mirageのデフォルトのペイロードは次のようなフォーマットであることがわかりました。
// GET /api/movies
{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}
しかし、もちろん、すべてのバックエンドAPIがこのフォーマットに一致するわけではありません。
例えば、APIがJSON:API仕様を使用しており、次のような場合もあります。
// GET /api/movies
{
"data": [
{
"id": 1,
"type": "movies",
"attributes": { "name": "Inception", "year": 2010 }
},
{
"id": 2,
"type": "movies",
"attributes": { "name": "Interstellar", "year": 2014 }
},
{
"id": 3,
"type": "movies",
"attributes": { "name": "Dunkirk", "year": 2017 }
}
]
}
そのため、Mirageシリアライザが存在します。シリアライザを使用すると、ルートハンドラー、モデル、リレーションシップ、その他のMirageの設定を変更することなく、レスポンスのフォーマットロジックをカスタマイズできます。
Mirageには、一般的なバックエンドフォーマットに一致するいくつかの名前付きシリアライザが付属しています。
import { createServer, JSONAPISerializer } from "miragejs"
createServer({
serializers: {
application: JSONAPISerializer,
},
})
基本クラスを拡張して、そのフォーマットフックを使用して独自のシリアライザを作成することもできます。
import { createServer, Serializer } from "miragejs"
createServer({
serializers: {
application: Serializer.extend({
keyForAttribute(attr) {
return dasherize(attr)
},
keyForRelationship(attr) {
return dasherize(attr)
},
}),
},
})
Mirageのシリアライザレイヤーはリレーションシップを認識しているため、サイドロードまたは関連データの埋め込みが期待されるエンドポイントのモックに役立ちます。
例えば、以下の設定では
createServer({
serializers: {
movie: Serializer.extend({
include: ["crewMembers"],
}),
},
routes() {
this.get("/movies/:id")
},
})
/movies/1
へのGETリクエストには、関連するクルーメンバーが自動的に含まれます。
// GET /movies/1
{
"movie": {
"id": 1,
"title": "Interstellar"
},
"crew-members": [
{
"id": 1,
"movie-id": 1,
"name": "Matthew McConaughey"
},
{
"id": 2,
"movie-id": 1,
"name": "Anne Hathaway"
},
{
"id": 3,
"movie-id": 1,
"name": "Jessica Chastain"
}
]
}
Mirageの名前付きシリアライザは、このような作業の大部分を代行するため、出発点として使用し、必要になった場合にのみAPI固有のカスタマイズを追加する必要があります。
パススルー
既存のアプリで作業している場合、またはAPI全体をモックしたくない場合でも、Mirageは優れたツールです。デフォルトでは、JavaScriptアプリが対応するルートハンドラーが定義されていないリクエストを行うと、Mirageはエラーをスローします。
これを回避するには、Mirageに未処理のリクエストを透過的に通過させるように指示します。
createServer({
routes() {
// Allow unhandled requests on the current domain to pass through
this.passthrough()
},
})
これで、既存のAPIに対して通常どおり開発できます。
新しい機能を構築する際には、APIの更新を待つ必要はありません。必要な新しいルートを定義するだけです。
createServer({
routes() {
// Mock this route and Mirage will intercept it
this.get("/movies")
// All other API requests on the current domain will still pass through
// e.g. GET /api/directors
this.passthrough()
// If your API requests go to an external domain, pass those through by
// specifying the fully qualified domain name
this.passthrough("http://api.acme.com/**")
},
})
機能を完全に開発およびテストできます。このようにして、サーバー定義を少しずつ構築し、サーバーの各状態に堅牢な受け入れテストを追加できます。
これで開始するには十分でしょう!
ドキュメントの次のセクションでは、Mirageの主要な概念をそれぞれ詳細に説明します。