Dynamic Tests From Cypress Fixture

Import a JSON fixture file to create dynamic Cypress tests

Often, you need to run the same test with different data. For example, one might want to test how the backend API handles creating an item for multiple items with different parameters. We could write a separate test for each item.

🔎 You can find the source code for this blog post in bahmutov/todo-graphql-example repo.

Hard-coded data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
describe('Creates each item', () => {
it('creates 1', () => {
cy.request({
body: {
title: 'use GraphQL',
completed: true
}
})
cy.visit('/')
cy.contains('.todo', 'use GraphQL')
})

it('creates 2', () => {
cy.request({
body: {
title: 'write React frontend',
completed: false
}
})
cy.visit('/')
cy.contains('.todo', 'write React frontend')
})

it('creates 3', () => {
cy.request({
body: {
title: 'nice',
completed: false
}
})
cy.visit('/')
cy.contains('.todo', 'nice')
})
})

Fixture data

The separate tests have a lot of repetitive code, and we are not even checking if the item is marked completed on the page! We could the items in the JSON file to be loaded and used in a single test.

cypress/fixtures/three.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"data": {
"allTodos": [
{
"id": "1",
"title": "use GraphQL",
"completed": true,
"__typename": "Todo"
},
{
"id": "2",
"title": "write React frontend",
"completed": false,
"__typename": "Todo"
},
{ "id": "21", "title": "nice", "completed": true, "__typename": "Todo" }
]
}
}

We can load the fixture file using cy.fixture command. Then we can iterate over the items, all in a single test.

cypress/integration/dynamic-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
it('in a single test', () => {
cy.fixture('three.json')
.its('data.allTodos')
.then((list) => {
list.forEach((item) => {
// clear all existing items
deleteAll()

// create the item using a network call
cy.request({
method: 'POST',
url: 'http://localhost:3000/',
body: {
operationName: 'AddTodo',
query: `
mutation AddTodo($title: String!, $completed: Boolean!) {
createTodo(title: $title, completed: $completed) {
id
}
}
`,
variables: {
title: item.title,
completed: item.completed,
},
},
})
// visit the page and check the item is present
cy.visit('/')
const classAssertion = item.completed
? 'have.class'
: 'not.have.class'
cy.contains('.todo', item.title).should(classAssertion, 'completed')
})
})
})

The above test is a little cumbersome to read, and if a single request goes wrong, the test stops without trying to testing other items from the list. We would like to have a separate test for each item instead.

Separate tests

cypress/integration/dynamic-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
describe('Creates each item', () => {
let items
// DOES NOT WORK, CANNOT ADD NEW TESTS
// AFTER THE RUN HAS STARTED
before(() => {
cy.fixture('three.json').then((data) => {
items = data.data.allTodos
})
})

// hmm, how do we create a test for each item?
})

☢️ If we load the list dynamically using cy.fixture or any other Cypress command, then the tests are already running, and it is too late to add new tests. Thus we need to load the fixture before any tests execute.

We can import the JSON file instead of using cy.fixture - that way the bundler will load the JSON file into the spec and the loaded data will be available to define tests.

cypress/integration/dynamic-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// import the fixture with the data for new tests
import { data } from '../fixtures/three.json'
import { deleteAll } from './utils'

describe('Creates each item', () => {
beforeEach(deleteAll)

// create a test for each item imported from the fixture
data.allTodos.forEach((item, k) => {
it(`creates item ${k + 1}`, () => {
// create the item using a network call
cy.request({
method: 'POST',
url: 'http://localhost:3000/',
body: {
operationName: 'AddTodo',
query: `
mutation AddTodo($title: String!, $completed: Boolean!) {
createTodo(title: $title, completed: $completed) {
id
}
}
`,
variables: {
title: item.title,
completed: item.completed,
},
},
})
// visit the page and check the item is present
cy.visit('/')
const classAssertion = item.completed ? 'have.class' : 'not.have.class'
cy.contains('.todo', item.title).should(classAssertion, 'completed')
})
})
})

The above approach creates a separate test for each item imported from the fixture. It is equivalent to having the items as a "static" list present in the spec file itself:

1
2
3
4
5
6
7
8
9
10
11
12
const list = [{
title: 'first test',
completed: false
}, {
title: 'second test',
completed: true
}]
list.forEach((item, k) => {
it(`creates item ${k + 1}`, () => {
// use the item
})
})

But I like having the data in a fixture file to clearly separate the test logic from the test data.

Video

I have recorded a short video explaining the above approach, see it below

See also