-
Notifications
You must be signed in to change notification settings - Fork 100
Description
I am trying to migrate from MockServer to mockttp and I'm having a few different issues getting mockttp to play nicely with Cypress. (I have already followed the discussion in #35 but the problems described here are different.)
Throughout this description I will refer to some branches which include test cases. The repository lives here: https://github.com/OliverJAsh/cypress-mockttp.
At the end I have a proposal to add a useConfig method, but first I'll describe the problem I'm having.
We want to start the mock server before all of our tests run. It seems like this is something we should do in a global before hook. This is defined outside of a describe block so it will run once before all tests.
import {getRemote} from 'mockttp'
const server = getRemote({ standaloneServerUrl: 'http://localhost:1773' })
before(async () => {
await server.start(8080)
})
describe('Mockttp serves mocked responses', () => {
it('to cy.request', () => {
cy.wrap(server.get("/mocked-path").thenReply(200, 'this is a mocked response'))
const url = 'http://localhost:8080/mocked-path'
cy.request(url).its('body').should('equal', 'this is a mocked response')
})
})(Branch: init)
In Cypress this works fine until we want to have multiple spec files. As soon as we have multiple spec files we need to move the before hook somewhere else, so it runs before all tests.
In Cypress it's recommended to define the global before in the cypress/support/index.js, as this is guaranteed to only run once before all spec files and tests. https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#:~:text=you%20can%20define%20behaviors%20in
However, if we do that then the spec files will not be able to reference the existing client instance (server)—each spec file will have to create its own:
cypress/support/index.js:
import {getRemote} from 'mockttp'
const server = getRemote({ standaloneServerUrl: 'http://localhost:1773' })
before(async () => {
await server.start(8080)
})
after(async () => {
await server.stop()
})dummy.spec.ts:
import {getRemote} from 'mockttp'
const server = getRemote({ standaloneServerUrl: 'http://localhost:1773' })
describe('Mockttp serves mocked responses', () => {
it('to cy.request', () => {
// This won't work because server has been started but this client instance
// hasn't been configured with the port.
cy.wrap(server.get("/mocked-path").thenReply(200, 'this is a mocked response'))
const url = 'http://localhost:8080/mocked-path'
cy.request(url).its('body').should('equal', 'this is a mocked response')
})
})(Branch: support)
This doesn't work either because the client won't work until you call start:
… and we can't call start in each spec file because the server has already been started by the client we created in cypress/support/index.js. We only want to configure the client in our spec to use the existing server that has already been started.
I hoped I might be able to move the initialised client (server) into a module and then import it from each spec file, e.g.:
server-client.js:
import {getRemote} from 'mockttp'
export const server = getRemote({ standaloneServerUrl: 'http://localhost:1773' })
console.log('registering before hook')
before(async () => {
await server.start(8080)
})
after(async () => {
await server.stop()
})dummy.spec.ts:
import { server } from "./server-client";
describe('Mockttp serves mocked responses', () => {
it('to cy.request', () => {
cy.wrap(server.get("/mocked-path").thenReply(200, 'this is a mocked response'))
const url = 'http://localhost:8080/mocked-path'
cy.request(url).its('body').should('equal', 'this is a mocked response')
})
})dummy2.spec.ts:
import { server } from "./server-client";
describe('Mockttp serves mocked responses', () => {
it('to cy.request', () => {
cy.wrap(server.get("/mocked-path").thenReply(200, 'this is a mocked response'))
const url = 'http://localhost:8080/mocked-path'
cy.request(url).its('body').should('equal', 'this is a mocked response')
})
})(Branch: import)
However, this doesn't work either, because the imported server-client.js module will be initialised not once but rather once for each spec file, because of the way Cypress works. This means the before hook is registered twice and thus we try to start the server twice. We can see this from the console.log I added:
We didn't have these problems when we were using MockServer because the client worked in a slightly different way. The client wasn't stateful, nor was it responsible for starting the mock server. Rather, with MockServer you have to configure it with port of a mock server which is already running. This meant we could create a new client as many times as we like without any of the issues I described above:
import { mockServerClient as createMockServerClient } from 'mockserver-client';
// This doesn't perform any side effects. It's just configuring the client.
// This means we can easily move it to a module and import it in each spec file.
const mock = createMockServerClient('localhost', 8080);
describe('MockServer serves mocked responses', () => {
it('to cy.request', () => {
cy.wrap(mock.mockSimpleResponse('/mocked-path', 'this is a mocked response', 200))
const url = 'http://localhost:8080/mocked-path'
cy.request(url).its('body').should('equal', 'this is a mocked response')
})
})This makes me think it would be good if mockttp worked in a similar way. Perhaps we could expose a new function on the client that just initialises the client with an existing mockServerConfig:
mockttp/src/client/mockttp-client.ts
Lines 292 to 302 in fc48bc4
| // Also open a stream connection, for 2-way communication we might need later. | |
| this.mockServerStream = await this.openStreamToMockServer(mockServerConfig); | |
| // Create a subscription client, preconfigured & ready to connect if on() is called later: | |
| this.prepareSubscriptionClientToMockServer(mockServerConfig); | |
| // We don't persist the config or resolve this promise until everything is set up | |
| this.mockServerConfig = mockServerConfig; | |
| // Load the schema on server start, so we can check for feature support | |
| this.mockServerSchema = (await this.queryMockServer<any>(introspectionQuery)).__schema; |
We would need to start the mock server outside of the tests. Usage in the tests would then look something like this:
dummy.spec.ts:
import {getRemote} from 'mockttp'
const server = getRemote({ standaloneServerUrl: 'http://localhost:1773' })
before(() => server.useConfig({ port: 8080, mockRoot: 'http://localhost:8080' }));
describe('Mockttp serves mocked responses', () => {
it('to cy.request', () => {
cy.wrap(server.get("/mocked-path").thenReply(200, 'this is a mocked response'))
const url = 'http://localhost:8080/mocked-path'
cy.request(url).its('body').should('equal', 'this is a mocked response')
})
})I have tested the idea by patching my node_modules locally and it seems to solve the problem. The branch useConfig-2 has a full reduced test case, including a patch that adds a useConfig method. (The patch is applied when you run yarn via the patch-package tool.)
What do you think? Perhaps you have a better idea of how to solve this!

