Cypress snippets
Cypress unit testing
Aside from browser-based testing, cypress is also just a neat test runner for other JS testing libraries. So we can use it to run unit tests. This is useful for testing store methods like getters and mutations that don't have any UI elements.
All of the information below is taken from this very cool blog post: https://dev.to/bahmutov/unit-testing-vuex-data-store-using-cypress-io-test-runner-3g4n The vuex testing docs are also good: https://vuex.vuejs.org/guide/testing.html and because cypress is just the test runner, we can use the syntax of the tests from there directly as well.
Unit test a store mutation
If our mutation is called increment
like so:
export const mutations = {
increment(state) {
...
},
}
import { mutations } from '../../src/counter'
describe('mutations', () => {
context('increment', () => {
const { increment } = mutations
it('INCREMENT', () => {
const state = { count: 0 }
increment(state)
// see https://on.cypress.io/assertions
// for "expect" examples
expect(state.count).to.equal(1)
})
})
})
Unit test a store getter
If we have defined a store getter like so
export const getters = {
myGetter (state, someArg ) {
return state.someVariable.filter(value => "someLogic")
}
}
Then we can test the getter like so:
import { getters } from './getters'
describe('getters', () => {
it('filteredProducts', () => {
// mock state
const state = {
someVariable: [
{ id: 1, title: 'Apple', category: 'fruit' },
{ id: 2, title: 'Orange', category: 'fruit' },
{ id: 3, title: 'Carrot', category: 'vegetable' }
]
}
// mock getter
const filterCategory = 'fruit'
// get the result from the getter
const result = getters.filteredProducts(state, { filterCategory })
// assert the result
expect(result).to.deep.equal([
{ id: 1, title: 'Apple', category: 'fruit' },
{ id: 2, title: 'Orange', category: 'fruit' }
])
})
})
See also: https://vuex.vuejs.org/guide/testing.html#testing-getters
Cypress Component test snippets
Cypress mounts the component using the vue-test-utils library.
That means, even if the Cypress documentation doesn't say so,
you can use anything that the vue-test-utils
documentation has
enabled.
For example, Cypress doesn't tell you how to write listeners
for
events emitted by your component, but you can look up in the
vue-test-utils docs how to do it.
Also take a look at:
- the Cypress component test docs (somewhat incomplete)
- vue-test-utils mount docs (Recall that Cypress has a slightly different syntax for assertions, using the cy.xyz()
commands.)
- the Vue testing handbook (This has some nice patterns, but is not specifically for Cypress.)
Also note that we are using Vue2 (specifically Nuxt2) and that many docs now start assuming that you use Vue3 (and thus different store libraries, component APIs, test library version, etc). So sometimes examples may be written for Vue3 and not work for us on Vue2.
Pass Info Into the Component
When a component relies on information being passed to it by other parts of the app, then we need to: 1. Simulate ("mock") this information when the component is tested in isolation during a component test. 2. Pass the simulated information to the component in a way that looks as if it came from the expected source.
Components in Vue can receive information in a number of different ways:
- as props
- via provide
/ inject
- from a global store
- ...
Below are examples for how you can mock each of these and pass them to the component during a component test.
Import a Component
import ComponentName from "~/components/component-name.vue";
Pass a Prop
cy.mount(ComponentName, {
propsData: {
myProp: "hasSomeValue"
}
});
Import Plugins
cy.mount(ComponentName, {
plugins: ["bootstrap-vue", "vue-select"]
});
#### Pass a Vuex Getter
```javascript
cy.mount(ComponentName, {
computed: {
myGetter: () => {
return "myGetterValue";
}
}
});
Pass a Vuex Getter that Takes an Argument (or Simulating a mapGetters Field)
cy.mount(ComponentName, {
computed: {
myGetter: () => (myGetterArgument) {
const myReturnValue = doSomethingWith(myGetterArgument);
return myReturnValue;
}
}
);
Note: It is usually better to completely mock the return value of a getter rather than trying to import the getter from the store. This is for three reasons:
- Getters in Vuex take iterative inputs (see e.g. here) in the store and unit tests (see here) but when we use them in components they behave like regular functions (see here). If we import a getter during non-e2e testing (i.e. when the app is not running), we would first have to turn the getter into a regular function ourselves by passing an also mocked store object to it.
- Getter can have other getters as dependencies. Not only will all of these getters have to be made into normal functions as well, but so do their dependencies in turn, and so on. Additionally all of these getters depend on different parts of the store to be mocked, so we can quickly approach a situation where we are almost mocking the entire app at runtime, just to make a component test "easier".
- Importing getters is less readable than just showing what information the mocked getter will give to the component. This is maybe the most important reason, because readability is key for our tests.
In short: mock getters as simple return objects everywhere. The only exception is when you are unit-testing the getter itself.
Pass a Vuex State Field (or Simulating a mapState Field)
cy.mount(ComponentName, {
mocks: {
$store: {
state: { myStateField: "myStateValue" }
}
}
});
Respond to information coming out of the component
Components may have to send information to other parts of the app when certain events occur or conditions are met. When the component is tested in isolation during a component test, these other parts of the app don't exist. However, during a component test we only care about the component. So the only thing we need to test is that the component emits the right type of information in the right situations. We don't have to simuate other parts and how they would respond to this information (this is done during full integration tests instead).
As with passing data to components, there are different ways data can come out of components:
- emitted events
- store mutations and actions
Below are some snippets for how you can listen to these types of data flows in a component test.
Listen to a Vuex State Mutation Being Committed
const mockStore = {
commit: () => { }
};
cy.spy(mockStore, "commit").as("commitSpy");
cy.mount(ComponentName, {
mocks: {
$store: mockStore
}
});
cy.get("something").click(); // Or, do something to evoke the mutation
cy.get("@commitSpy").should("have.been.calledOnce");
You can chain assertions off of should
, see the docs.
Listen to a Vuex State Action Being Dispatched
const mockStore = {
dispatch: () => { }
};
cy.spy(mockStore, "dispatch").as("dispatchSpy");
cy.mount(ComponentName, {
mocks: {
$store: mockStore
}
});
cy.get("something").click(); // Or, do something to evoke the mutation
cy.get("@commitSpy").should("have.been.calledOnce");
Check that a Specific Payload was Sent with the Mutation / Action
const mockStore = {
commit: () => { }
};
cy.spy(mockStore, "commit").as("commitSpy");
cy.mount(ComponentName, {
mocks: {
$store: mockStore
}
});
cy.get("something").click(); // Or, do something to evoke the mutation
cy.get("@commitSpy").should("have.been.calledWith", "myMutationName", {
payloadField: "payLoadValue"});
Listen for an emitted event
const mySpy = cy.spy().as("mySpy");
cy.mount(ComponentName, {
listeners: {
emitEventName: mySpy,
}
});
// Perform an interaction to evoke the emit
cy.get(`.<class-name>`).click();
// Check to see if emit was called
cy.get("@mySpy").should("have.been.called");
// Something to note is that 'emitEventName' must maintain the type of case used by the component (i.e. kebab-case, or camel case)
Check an emitted event's payload
const mySpy = cy.spy().as("mySpy");
cy.mount(ComponentName, {
listeners: {
emitEventName: mySpy,
}
});
// Perform an interaction to evoke the emit
cy.get(`.<class-name>`).click();
// Check to see if emit was called with the correct payload
cy.get("@mySpy").should("have.been.calledWith", {
payloadField: "payLoadValue"
});
// Something to note is that 'emitEventName' must maintain the type of case used by the component (i.e. kebab-case, or camel case)
Listen for a method call
cy.spy(ComponentName.methods, 'methodName').as("mySpy");
cy.mount(ComponentName);
// Perform an interaction to evoke the method
cy.get(`.<class-name>`).click();
// Check to see if method was called
cy.get("@mySpy").should("have.been.called");
// Something to note is that 'methodName' must maintain the type of case used by the component (i.e. kebab-case, or camel case)
Check a method call's payload
// Given methodName(value-1, value-2)
cy.spy(ComponentName.methods, 'methodName').as("mySpy");
cy.mount(ComponentName);
// Perform an interaction to evoke the method
cy.get(`.<class-name>`).click();
// Check to see if method was called
cy.get("@mySpy").should("have.been.called", "value-1", "value-2");
// Something to note is that 'methodName' must maintain the type of case used by the component (i.e. kebab-case, or camel case)
Provide different responses based on a condition when intercepting requests
The following snippet is from Checkbox.cy.js
in the query tool. When the submitQuery
button is clicked for the first time the condition isFirstClick
is set to false and response
is replied by cy.intercept
and thereafter every click of the submitQuery
button will result in response2
being replied.
let isFirstClick = true;
cy.intercept('GET', 'query/*', (req) => {
if (isFirstClick) {
isFirstClick = false;
req.reply(response);
} else {
req.reply(response2);
}
});