cy.intercept
vs MSW
Jan 1st 2023
October 13th 2023
Same test, different APIs.
html vs the browser...
/** 5 imports **/
it('can render with redux with defaults', () => {
cy.mount(
<Provider store={store}>
<Counter />
</Provider>
)
cy.getByCy('+').click()
cy.findByLabelText('count').should('have.text', '1')
})
it('can render with redux with custom initial state', () => {
const store = createStore(reducer, { count: 3 })
cy.mount(
<Provider store={store}>
<Counter />
</Provider>
)
cy.getByCy('-').click()
cy.findByLabelText('count').should('have.text', '2')
})
/* 8 imports */
test('can render with redux with defaults', () => {
render(
<Provider store={store}>
<Counter />
</Provider>,
)
userEvent.click(screen.getByText('+'))
expect(screen.getByLabelText(/count/i)).toHaveTextContent(1)
})
test('can render with redux with custom initial state', () => {
const store = createStore(reducer, {count: 3})
render(
<Provider store={store}>
<Counter />
</Provider>,
)
userEvent.click(screen.getByText('-'))
expect(screen.getByLabelText(/count/i)).toHaveTextContent('2')
})
import 'cypress-axe'
/* import InaccessibleForm */
it('checks all violations', () => {
cy.mount(<InaccessibleForm />)
cy.injectAxe()
cy.checkAlly()
})
it('checks filtered violations', () => {
cy.mount(<InaccessibleForm />)
cy.injectAxe()
cy.checkA11y(null, {
includedImpacts: ['moderate', 'serious']
})
})
import {axe} from 'jest-axe'
/* import InaccessibleForm */
test('all violations', async () => {
const {container} = render(<InaccessibleForm />)
expect(await axe(container)).toHaveNoViolations()
})
it('checks filtered violations', async () => {
const { container } = render(<InaccessibleForm />);
const results = await axe(container, {
runOnly: {
type: 'rule',
values: ['moderate', 'serious']
}
})
expect(results).toHaveNoViolations();
})
it('displays the users current location', () => {
const fakePosition = {
coords: {
latitude: 35,
longitude: 139
}
}
cy.window().then((win) =>
cy
.stub(win.navigator.geolocation, 'getCurrentPosition')
.callsArgWith(0, fakePosition)
.as('getCurrentPosition')
)
cy.mount(<Location />)
cy.get('@getCurrentPosition').should('be.called')
cy.contains(fakePosition.coords.latitude)
cy.contains(fakePosition.coords.longitude)
})
// util to create a promise that you can resolve/reject on demand.
function deferred() {
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
return {promise, resolve, reject}
}
test('displays the users current location', async () => {
const fakePosition = {
coords: {
latitude: 35,
longitude: 139,
},
}
const {promise, resolve} = deferred()
window.navigator.geolocation = {
getCurrentPosition: jest.fn(),
}
window.navigator.geolocation.getCurrentPosition.mockImplementation(callback =>
promise.then(() => callback(fakePosition)),
)
render(<Location />)
await act(async () => {
resolve()
await promise
})
expect(window.navigator.geolocation.getCurrentPosition).toHaveBeenCalled()
expect(screen.getByText(/latitude/i)).toHaveTextContent(
`Latitude: ${fakePosition.coords.latitude}`,
)
expect(screen.getByText(/longitude/i)).toHaveTextContent(
`Longitude: ${fakePosition.coords.longitude}`,
)
})
cy.intercept
vs MSW
to.have.been.called
toHaveBeenCalled
cy.spy(obj, 'method')
jest.spyOn(obj, 'method')
have.been.calledTwice
its('callCount').should('eq', 2)
toHaveBeenCalledTimes(2)
sinon.match.string
expect.any(String)
const spy = cy.spy(calculator, "add").as("add")
calculator.add(2, 3)
const isEven = (x: number) => x % 2 === 0
const isOdd = (x: number) => x % 2 === 1
const { match } = Cypress.sinon
expect(spy).to.be.calledWith(match(isEven), match(isOdd))
const spy = jest.spyOn(calculator, 'add')
calculator.add(2, 3)
const isEven = (x: number) => x % 2 === 0
const isOdd = (x: number) => x % 2 === 1
expect.extend({
toBeEven(received) {
const pass = isEven(received)
if (pass) {
return {
message: () => `expected ${received} not to be an even number`,
pass: true,
}
} else {
return {
message: () => `expected ${received} to be an even number`,
pass: false,
}
}
},
toBeOdd(received) {
const pass = isOdd(received)
if (pass) {
return {
message: () => `expected ${received} not to be an odd number`,
pass: true,
}
} else {
return {
message: () => `expected ${received} to be an odd number`,
pass: false,
}
}
},
})
expect(spy.mock.calls[0][0]).toBeEven()
expect(spy.mock.calls[0][1]).toBeOdd()
it("resolved value (promises)", () => {
const calc = {
add(a: number, b: number) {
return Cypress.Promise.resolve(a + b)
},
}
cy.spy(calc, "add").as("add")
// wait for the promise to resolve then confirm its resolved value
cy.wrap(calc.add(4, 5)).should("equal", 9)
cy.wrap(calc.add(1, 90)).should("equal", 91)
cy.wrap(calc.add(-5, -8)).should("equal", -13)
// example of confirming one of the calls used add(4, 5)
cy.get("@add").should("have.been.calledWith", 4, 5)
cy.get("@add").should("have.been.calledWith", 1, 90)
cy.get("@add").should("have.been.calledWith", -5, -8)
// now let's confirm the resolved values
// first we need to wait for all promises to resolve
cy.get("@add")
.its("returnValues")
.then((ps) => Promise.all(ps))
.should("deep.equal", [9, 91, -13])
})
it("resolved value (promises)", async () => {
const calc = {
add(a: number, b: number) {
return new Promise((resolve) => {
resolve(a + b)
})
},
}
const spy = jest.spyOn(calc, "add")
// Let's gather the promises first
const promises = [calc.add(4, 5), calc.add(1, 90), calc.add(-5, -8)]
// Now we wait for all the promises to resolve
const results = await Promise.all(promises)
expect(spy).toHaveBeenNthCalledWith(1, 4, 5)
expect(spy).toHaveBeenNthCalledWith(2, 1, 90)
expect(spy).toHaveBeenNthCalledWith(3, -5, -8)
expect(results).toEqual([9, 91, -13])
});
cy.stub(obj, 'method')
jest.spyOn(obj, 'method')
.mockImplementation(jest.fn())
cy.stub()
jest.fn()
cy.stub(obj, 'method')
.returns('foo')
jest.spyOn(obj, 'method')
.mockReturnValue('foo')
cy.stub(obj, 'method')
.withArgs('bar')
.returns('foo')
jest.spyOn(obj, 'method')
.mockImplementation(arg => {
if (arg === 'bar') return 'foo'
})
cy.stub(obj, 'method')
.resolves('foo')
jest.spyOn(obj, 'method')
.mockImplementation(() =>
Promise.resolve('foo')
)
cy.stub(obj, 'method')
.rejects(new Error('foo'))
jest.spyOn(obj, 'method')
.mockImplementation(() =>
Promise.reject(new Error('foo'))
)
Restoring the original method after stub
Matchers: .callThrough(), withArgs(), match.type, match(predicate)
Calling the original method from the stub
Controlling time cy.clock
vs jest.useFakeTimers
cy.intercept
vs MSW
our code
network requests
mock here
less here
+ custom mount
it("should see error on initial load with GET", () => {
// in comparison, this is all we have to do with cyct
cy.intercept("GET", `${Cypress.env("API_URL")}/heroes`, {
statusCode: 400,
delay: 100,
}).as("notFound")
// test code...
// no clean up needed
})
it("should see error on initial load with GET", async () => {
// have to define handlers, setup server and listen
const handlers = [
rest.get(
`${process.env.REACT_APP_API_URL}/heroes`,
async (_req, res, ctx) => res(ctx.status(400))
),
]
const server = setupServer(...handlers);
server.listen({
onUnhandledRequest: "warn",
})
// test code...
// have to clean up
server.resetHandlers();
server.close();
});
context("200 flows", () => {
beforeEach(() => {
// the GET is common to both tests
cy.intercept("GET", `${Cypress.env("API_URL")}/heroes`, {
fixture: "heroes.json",
}).as("getHeroes")
cy.wrappedMount(<Heroes />)
})
it("should display the hero list on render...", () => {
// test code
})
it("should go through the modal flow, and cover error on DELETE", () => {
// test code
// DELETE mock is unique to this test
// we can define it or change our network mock on the fly
cy.intercept("DELETE", "*", { statusCode: 500 }).as("deleteHero")
invokeHeroDelete()
// test code
})
})
describe("200 flows", () => {
const handlers = [
rest.get(
`${process.env.REACT_APP_API_URL}/heroes`,
async (_req, res, ctx) => res(ctx.status(200), ctx.json(heroes))
),
// we have to have all definitions in the handler
rest.delete(
`${process.env.REACT_APP_API_URL}/heroes/${heroes[0].id}`, // use /.*/ for all requests
async (_req, res, ctx) => res(ctx.status(500), ctx.json("expected error"))
),
]
const server = setupServer(...handlers)
beforeAll(() => {
server.listen({
onUnhandledRequest: "warn",
})
})
beforeEach(() => wrappedRender(<Heroes />))
afterEach(server.resetHandlers)
afterAll(server.close)
it("should display the hero list on render...", async () => {
// test code
})
it("should go through the modal flow, and cover error on DELETE", async () => {
// test code
// uses the network definition in the beginning
invokeHeroDelete()
// test code
})
})
cy.intercept
vs MSW
// ./src/components/HeaderBarBrand.ts
<NavLink data-cy="navLink" to="/" className="navbar-item navbar-home">
<span className="tour">TOUR</span>
{/* Change OF to ON */}
<span className="of">ON</span>
<span className="heroes">HEROES</span>
</NavLink>
// ./src/components/InputDetail.ts
<input
name={name}
role={name}
defaultValue={shownValue}
placeholder={placeholder}
// add a setTimeout to simulate an asynchronous delay
onChange={() => setTimeout(onChange, 3000)}
readOnly={readOnly}
className="input"
type="text"
></input>
// ./src/heroes/Heroes.ts
const handleDeleteFromModal = () => {
// heroToDelete ? deleteHero(heroToDelete) : null
setShowModal(false)
}
cy.intercept
vs MSW
There are many similarities between CyCT & RTL; if we know RTL & some Cypres e2e, we will be very comfortable with CyCT
Declarative convenience vs imperative possibility
The key differentiator is the developer experience;
real browser vs html
full observability of the app vs working in the dark