Import Cypress fixtures

How to refactor loading JSON fixtures for simplicity

In bahmutov/sudoku I am loading two JSON fixtures to mock a method that creates the new board. This ensures the game always starts with the same numbers and generates the same image for visual testing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react'
import { App } from './App'
import { mount } from 'cypress-react-unit-test'
import * as UniqueSudoku from './solver/UniqueSudoku'
describe('App', () => {
it('mocks board creation', () => {
cy.fixture('init-array').then(initArray => {
cy.fixture('solved-array').then(solvedArray => {
cy.stub(UniqueSudoku, 'getUniqueSudoku').returns([initArray, solvedArray])
})
})
cy.clock()
mount(<App />)
cy.get('.game__cell--filled').should('have.length', 45)
cy.get('.container').matchImageSnapshot('same-game-mocked-sudoku')
})
})

Mocked board creation produces the same Sudoku game

The JSON fixtures are simple arrays, we are using cy.fixture command to load them.

1
2
3
4
// cypress/fixtures/init-array.json
["0", "0", "9", "0", "2", "0", "0", ...]
// cypress/fixtures/solved-array.json
["6", "7", "9", "3", "2", "8", "4", ...]

After the first test passes, we can write a test that plays one or several moves, then another test that sets the entire board and wins the game, etc. All these tests will load the same two fixture files at the start.

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
37
describe('App', () => {
it('mocks board creation', () => {
cy.fixture('init-array').then(initArray => {
cy.fixture('solved-array').then(solvedArray => {
cy.stub(UniqueSudoku, 'getUniqueSudoku').returns([initArray, solvedArray])
})
})
cy.clock()
mount(<App />)
...
})

it('plays a move', () => {
cy.fixture('init-array').then(initArray => {
cy.fixture('solved-array').then(solvedArray => {
cy.stub(UniqueSudoku, 'getUniqueSudoku').returns([initArray, solvedArray])
})
})
cy.clock()
mount(<App />)
...
})

it('wins the game', () => {
// load the solved array
// and set the initial array to be same without one move
cy.fixture('solved-array').then(solvedArray => {
const almostSolved = [...solvedArray]
// by setting entry to "0" we effectively clear the cell
almostSolved[0] = '0'
cy.stub(UniqueSudoku, 'getUniqueSudoku').returns([almostSolved, solvedArray])
})
cy.clock()
mount(<App />)
...
})
})

beforeEach

Loading the same fixture files in every test?! We can do better. First, let's load the fixtures in the beforeEach hook and save them in the test context. We will need to change our test functions from arrow functions to actual function functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
beforeEach(() => {
cy.fixture('init-array').as('initArray')
cy.fixture('solved-array').as('solvedArray')
})

it('mocks board creation', function () {
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([this.initArray, this.solvedArray])
cy.clock()
mount(<App />)
...
})

it('plays a move', function () {
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([this.initArray, this.solvedArray])
cy.clock()
mount(<App />)
...
})

Cypress .as command saves the loaded object in the test context, which is available as a property during the test.

before

The hook beforeEach runs with every test, can we load the fixtures once? We can run the code once using before hook, but that will cause a problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
before(() => {
cy.fixture('init-array').as('initArray')
cy.fixture('solved-array').as('solvedArray')
})

it('mocks board creation', function () {
// ✅ works
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([this.initArray, this.solvedArray])
cy.clock()
mount(<App />)
...
})

it('plays a move', function () {
// 🔥 DOES NOT WORK
// this.initArray is undefined
// this.solvedArray is undefined
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([this.initArray, this.solvedArray])
cy.clock()
mount(<App />)
...
})

The test runner runs the before hook before the very first test, sets the fixtures as properties, and the first test passes. Then the test runner clears the test context and runs the second test. Thus during the second test, the properties initArray and solvedArray are undefined.

To get around this problem, let's use closure variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let initArray
let solvedArray
before(() => {
cy.fixture('init-array').then(arr => initArray = arr)
cy.fixture('solved-array').then(arr => solvedArray = arr)
})

it('mocks board creation', () => {
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([initArray, solvedArray])
cy.clock()
mount(<App />)
...
})

it('plays a move', () => {
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([initArray, solvedArray])
cy.clock()
mount(<App />)
...
})

I strongly recommend using closure variables instead of this properties. The closure variables are clearly visible and do not depend on function vs () => {} syntax.

imports

Another possible solution for JSON fixtures it to ... import them from the fixture file. Cypress will bundle the JSON files just fine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import initArray from '../cypress/fixtures/init-array.json'
import solvedArray from '../cypress/fixtures/solved-array.json'

it('mocks board creation', () => {
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([initArray, solvedArray])
cy.clock()
mount(<App />)
...
})

it('plays a move', () => {
cy.stub(UniqueSudoku, 'getUniqueSudoku')
.returns([initArray, solvedArray])
cy.clock()
mount(<App />)
...
})

Happy testing!