Mock Network Requests in Cypress with Mirage
Use your Mirage server to test your application under different server scenarios using Cypress.
This is a quickstart guide for people already using Cypress in their apps.
Step 1: Install Mirage
First, make sure you have Mirage installed:
# Using npm
npm install --save-dev miragejs
# Using Yarn
yarn add --dev miragejs
Step 2: Define your server
Create a new src/server.js
file and define your mock server.
Here's a basic example:
// src/server.js
import { createServer, Model } from "miragejs"
export function makeServer({ environment = "development" } = {}) {
let server = createServer({
environment,
models: {
user: Model,
},
seeds(server) {
server.create("user", { name: "Bob" })
server.create("user", { name: "Alice" })
},
routes() {
this.namespace = "api"
this.get("/users", (schema) => {
return schema.users.all()
})
},
})
return server
}
Step 3: Have Cypress define a proxy function for your app's API requests
Add the following code to your cypress/support/index.js
file:
// cypress/support/index.js
Cypress.on("window:before:load", (win) => {
win.handleFromCypress = function (request) {
return fetch(request.url, {
method: request.method,
headers: request.requestHeaders,
body: request.requestBody,
}).then((res) => {
let content = res.headers.get("content-type").includes("application/json")
? res.json()
: res.text()
return new Promise((resolve) => {
content.then((body) => resolve([res.status, res.headers, body]))
})
})
}
})
This code defines a handleFromCypress
function on your application's window
object. In the next step, we'll configure your app to call this function whenever it makes a network request while Cypress is running.
Step 4: Proxy your app's network requests
In your app's bootstrapping file, use Mirage to proxy your app's API requests to the handleFromCypress
function when Cypress is running.
Create React App users, this code goes in
src/index.js
Vue CLI users, this code goes in
src/main.js
.
import { createServer, Response } from "miragejs"
if (window.Cypress) {
// If your app makes requests to domains other than / (the current domain), add them
// here so that they are also proxied from your app to the handleFromCypress function.
// For example: let otherDomains = ["https://my-backend.herokuapp.com/"]
let otherDomains = []
let methods = ["get", "put", "patch", "post", "delete"]
createServer({
environment: "test",
routes() {
for (const domain of ["/", ...otherDomains]) {
for (const method of methods) {
this[method](`${domain}*`, async (schema, request) => {
let [status, headers, body] = await window.handleFromCypress(
request
)
return new Response(status, headers, body)
})
}
}
// If your central server has any calls to passthrough(), you'll need to duplicate them here
// this.passthrough('https://analytics.google.com')
},
})
}
Now, whenever Cypress boots up your app, this code will delegate your app's network requests to the handleFromCypress
function that we defined in the previous step.
Once we start our real configured Mirage server alongside our Cypress code, it will start intercepting the requests from that function.
Step 5: Write tests using your Mirage server
Create a new cypress/integration/app.spec.js
file, import your makeServer
function, and start and shutdown Mirage before and after each test. You can then seed Mirage with a different data scenario in each test, and use the test to assert against the state of your UI.
import { makeServer } from "../../src/server"
describe("user list", () => {
let server
beforeEach(() => {
server = makeServer({ environment: "test" })
})
afterEach(() => {
server.shutdown()
})
it("shows the users from our server", () => {
server.create("user", { id: 1, name: "Luke" })
server.create("user", { id: 2, name: "Leia" })
cy.visit("/")
cy.get('[data-testid="user-1"]').contains("Luke")
cy.get('[data-testid="user-2"]').contains("Leia")
})
it("shows a message if there are no users", () => {
// Don't create any users
cy.visit("/")
cy.get('[data-testid="no-users"]').should("be.visible")
})
})
Note that we pass in environment: test
option into our makeServer
function, so that Mirage doesn't load its database seeds. That way, the server starts out empty for each test run, and in the beginning of our tests we can use server.create
to set up our data scenario. The test environment also disables logging and latency, so that by default your CI test logs will be clean and your tests will be fast.
Also note our usage of Cypress's describe
blocks, as they will keep our Mirage server scoped to each spec as we add more files, preventing any state leakage between test files.
Step 6: Alter your Mirage server to test different server states
In addition to different data scenarios, you can use your tests to reconfigure your Mirage server to test new situations.
For example, you can test an error state like this:
import { Response } from "miragejs"
it("handles error responses from the server", () => {
// Override Mirage's route handler for /users, just for this test
server.get("/users", () => {
return new Response(500, {}, { error: "The database is on vacation." })
})
cy.visit("/")
cy.get('[data-testid="server-error"]').contains(
"The database is on vacation."
)
})
Because of the way Mirage integrates with Cypress each test will get a fresh Mirage server based on your main server definition. Any overrides you make within a test will be isolated to that test.