Personal portfolio - Mauricio Aznar

React testing (Bonnie Shulkin)

 

Introduction

setupTests.js

import Enzyme, {shallow} from 'enzyme'
import EnzymeAdapter from 'enzyme-adapter-react-16'

Enzyme.configure({
  adapter: new EnzymeAdapter(),
  disableLifecycleMethods: true
})

utils.js

import checkPropTypes from 'check-prop-types'
import {createStore, applyMiddleware} from 'redux'

import rootReducer from '../src/reducers'
import {middlewares} from '../src/configureStore'

export const storeFactory = (initialState) => {
  const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)

  return createStoreWithMiddleware(rootReducer, initialState)
}

export const findByTestAttr = (wrapper, val) => {
  return wrapper.find(`[data-test="${val}"]`)
}

export const checkProps = (component, conformingProps) => {
  const propError = checkPropTypes(
    component.propTypes,
    conformingProps,
    'prop',
    component.name
  )
  expect(propError).toBeUndefined()
}

package.json

{
  "name": "jotto",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@babel/core": "7.12.3",
    "@pmmmwh/react-refresh-webpack-plugin": "0.4.2",
    "@svgr/webpack": "5.4.0",
    "@testing-library/jest-dom": "^5.11.5",
    "@testing-library/react": "^11.1.2",
    "@testing-library/user-event": "^12.2.2",
    "@typescript-eslint/eslint-plugin": "^4.5.0",
    "@typescript-eslint/parser": "^4.5.0",
    "axios": "^0.21.0",
    "babel-eslint": "^10.1.0",
    "babel-jest": "^26.6.0",
    "babel-loader": "8.1.0",
    "babel-plugin-named-asset-import": "^0.3.7",
    "babel-preset-react-app": "^10.0.0",
    "bfj": "^7.0.2",
    "camelcase": "^6.1.0",
    "case-sensitive-paths-webpack-plugin": "2.3.0",
    "css-loader": "4.3.0",
    "dotenv": "8.2.0",
    "dotenv-expand": "5.1.0",
    "eslint": "^7.11.0",
    "eslint-config-react-app": "^6.0.0",
    "eslint-plugin-flowtype": "^5.2.0",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jest": "^24.1.0",
    "eslint-plugin-jsx-a11y": "^6.3.1",
    "eslint-plugin-react": "^7.21.5",
    "eslint-plugin-react-hooks": "^4.2.0",
    "eslint-plugin-testing-library": "^3.9.2",
    "eslint-webpack-plugin": "^2.1.0",
    "file-loader": "6.1.1",
    "fs-extra": "^9.0.1",
    "html-webpack-plugin": "4.5.0",
    "identity-obj-proxy": "3.0.0",
    "jest": "26.6.0",
    "jest-circus": "26.6.0",
    "jest-resolve": "26.6.0",
    "jest-watch-typeahead": "0.6.1",
    "mini-css-extract-plugin": "0.11.3",
    "optimize-css-assets-webpack-plugin": "5.0.4",
    "pnp-webpack-plugin": "1.6.4",
    "postcss-flexbugs-fixes": "4.2.1",
    "postcss-loader": "3.0.0",
    "postcss-normalize": "8.0.1",
    "postcss-preset-env": "6.7.0",
    "postcss-safe-parser": "5.0.2",
    "prop-types": "^15.7.2",
    "react": "^16.14.0",
    "react-app-polyfill": "^2.0.0",
    "react-dev-utils": "^11.0.0",
    "react-dom": "^16.14.0",
    "react-redux": "^7.2.2",
    "react-refresh": "^0.8.3",
    "redux": "^4.0.5",
    "redux-thunk": "^2.3.0",
    "resolve": "1.18.1",
    "resolve-url-loader": "^3.1.2",
    "sass-loader": "8.0.2",
    "semver": "7.3.2",
    "style-loader": "1.3.0",
    "terser-webpack-plugin": "4.2.3",
    "ts-pnp": "1.2.0",
    "url-loader": "4.1.1",
    "web-vitals": "^0.2.4",
    "webpack": "4.44.2",
    "webpack-dev-server": "3.11.0",
    "webpack-manifest-plugin": "2.2.0",
    "workbox-webpack-plugin": "5.1.4"
  },
  "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "babel-plugin-react-remove-properties": "^0.3.0",
    "check-prop-types": "^1.1.2",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.5",
    "jest-enzyme": "^7.1.2",
    "moxios": "^0.4.0"
  },
  "jest": {
    "roots": [
      "<rootDir>/src"
    ],
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "setupFiles": [
      "react-app-polyfill/jsdom"
    ],
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js"
    ],
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
    ],
    "testEnvironment": "jsdom",
    "testRunner": "D:\\Projects\\Javascript\\jotto\\node_modules\\jest-circus\\runner.js",
    "transform": {
      "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "modulePaths": [],
    "moduleNameMapper": {
      "^react-native$": "react-native-web",
      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
    },
    "moduleFileExtensions": [
      "web.js",
      "js",
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ],
    "watchPlugins": [
      "jest-watch-typeahead/filename",
      "jest-watch-typeahead/testname"
    ],
    "resetMocks": true
  },
  "babel": {
    "env": {
      "production": {
        "plugins": [
          [
            "react-remove-properties",
            {
              "properties": [
                "data-test"
              ]
            }
          ]
        ]
      }
    },
    "presets": [
      "react-app"
    ]
  }
}

Component tests

Input

Input.js

import React, {Component} from 'react'

import {connect} from 'react-redux'
import {guessWord, reset} from './actions'

export class UnconnectedInput extends Component {
  constructor(props) {
    super(props);

    //initialize the state
    this.state = {
      currentGuess: ''
    }

    this.handleOnSubmit = this.handleOnSubmit.bind(this)
  }

  handleOnSubmit (e) {
    e.preventDefault()
    const guessedWord = this.state.currentGuess
    if (guessedWord && guessedWord.length > 0) {
      this.props.guessWord(this.state.currentGuess)
      this.setState({currentGuess: ''})
    }
  }

  render() {

    const contents = !this.props.success
      ? <form>
          <input
            data-test="input-box"
            className="mb-2 mx-sm-3"
            value={this.state.currentGuess}
            onChange={(e) => {
              this.setState({currentGuess: e.target.value})
            }}
          />
          <button
            data-test="submit-button"
            type="submit"
            className="btn btn-primary mb-2"
            onClick={(e) => {this.handleOnSubmit(e)}}
          >
            Submit
          </button>
        </form>
      : <form>
          <button
            data-test="reset-button"
            className="btn btn-secondary ml-2"
          >
            Reset
          </button>
      </form>

    return <div data-test="component-input">
      {contents}
    </div>
  }
}

const mapStateToProps = ({success}) => {
  return {
    success
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    guessWord: guessWord,
    reset: reset
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedInput)

Input.test.js

import React from 'react'
import {shallow} from 'enzyme'

import {findByTestAttr, storeFactory} from '../test/testUtils'
import Input, {UnconnectedInput} from './Input'
import {guessWord} from './actions'

const setup = (initialState = {}) => {
  const store = storeFactory(initialState)
  return shallow(<Input store={store} />).dive().dive()
}

describe('render', () => {
  describe('word has not been guessed', () => {
    let wrapper
    beforeEach(() => {
      const initialState = {success: false}
      wrapper = setup(initialState)
    })
    test('renders component without error', () => {
      const component = findByTestAttr(wrapper, 'component-input')
      expect(component.length).toBe(1)
    })
    test('renders input box', () => {
      const inputBox = findByTestAttr(wrapper, 'input-box')
      expect(inputBox.length).toBe(1)
    })
    test('renders submit button', () => {
      const submitButton = findByTestAttr(wrapper, 'submit-button')
      expect(submitButton.length).toBe(1)
    })
    test('does not render reset button', () => {
      const submitButton = findByTestAttr(wrapper, 'reset-button')
      expect(submitButton.length).toBe(0)
    })
  })
  describe('word has been guessed', function () {
    let wrapper
    beforeEach(() => {
      wrapper = setup({success: true})
    })
    test('renders component without error', () => {
      const component = findByTestAttr(wrapper, 'component-input')
      expect(component.length).toBe(1)
    })
    test('does not render input box', () => {
      const inputBox = findByTestAttr(wrapper, 'input-box')
      expect(inputBox.length).toBe(0)
    })
    test('does not render submit button', () => {
      const submitButton = findByTestAttr(wrapper, 'submit-button')
      expect(submitButton.length).toBe(0)
    })
    test('render reset button', () => {
      const submitButton = findByTestAttr(wrapper, 'reset-button')
      expect(submitButton.length).toBe(1)
    })
  });
})

describe('redux props', () => {
  test('has success piece of state as prop', () => {
    const success = true
    const wrapper = setup({success})
    const successProp = wrapper.instance().props.success
    expect(successProp).toBe(success)
  })
  test('`guessWord` action creator is a function prop', () => {
    const success = true
    const wrapper = setup({success})
    const guessWordProp = wrapper.instance().props.guessWord
    expect(guessWordProp).toBeInstanceOf(Function)
  })
})

describe('`guessWord` action creator call', () => {
  let guessWordMock;
  let wrapper;
  const guessedWord = 'train'

  beforeEach(() => {
    // create a mock function for `getSecretWord`
    guessWordMock = jest.fn()

    const props = {guessWord: guessWordMock, success: false}

    wrapper = shallow(<UnconnectedInput {...props} />)

    //add value to input box
    wrapper.setState({currentGuess: guessedWord})

    const button = findByTestAttr(wrapper, 'submit-button')

    //simulate click on submit button
    button.simulate('click', {preventDefault() {}})
  })


  test('calls `guessWord` when button is clicked', () => {
    const guessWordMockCount = guessWordMock.mock.calls.length

    expect(guessWordMockCount).toBe(1)
  })

  test('calls `guessWord with input value as argument`', () => {
    const guessWordArg = guessWordMock.mock.calls[0][0]
    expect(guessWordArg).toBe(guessedWord)
  })

  test('clear input box on submit', () => {
    expect(wrapper.state('currentGuess')).toBe('')
  })
})

Normal component with props #1

GuessedWords.js

import React from 'react'
import PropTypes from 'prop-types'

const GuessedWords = (props) => {
  let contents
  if (props.guessedWords.length === 0) {
    contents = (
      <span data-test="guess-instructions">
        Try to guess the secret word!
      </span>
    )
  } else {
    const guessedWordsRows = props.guessedWords.map((word, index) => {
      return (
        <tr key={index} data-test="guessed-word">
          <td>
            {word.guessedWord}
          </td>
          <td>
            {word.letterMatchCount}
          </td>
        </tr>
      )
    })
    contents = (
      <div data-test="guessed-words">
        <h3>Guessed words</h3>
        <table className="table table-sm">
          <thead className="thead-light">
            <tr>
              <th>
                Guess
              </th>
              <th>
                Matching Letters
              </th>
            </tr>
          </thead>
          <tbody>
            {guessedWordsRows}
          </tbody>
        </table>
      </div>
    )
  }
  return (
    <div data-test="component-guessed-words">
      {contents}
    </div>
  )
}

GuessedWords.propTypes = {
  guessedWords: PropTypes.arrayOf(
    PropTypes.shape({
      guessedWord: PropTypes.string.isRequired,
      letterMatchCount: PropTypes.number.isRequired
    })
  ).isRequired
}

export default GuessedWords

GuessedWords.test.js

import React from 'react'
import {shallow} from 'enzyme'
import {findByTestAttr, checkProps} from '../test/testUtils'
import GuessedWords from './GuessedWords'

const defaultProps = {
    guessedWords: [{guessedWord: 'train', letterMatchCount: 3}]
  }


const setUp = (props = {}) => {
  const setUpProps = {...defaultProps, ...props}
  return shallow(<GuessedWords {...setUpProps} />)
}

test('does not throw warning with expected props', () => {
  checkProps(GuessedWords, defaultProps)
})

describe('if there are no words guessed', () => {

  let wrapper
  beforeEach(() => {
    wrapper = setUp({guessedWords: []})
  })

  test('renders without error', () => {
    const component = findByTestAttr(wrapper, 'component-guessed-words')
    expect(component.length).toBe(1)
  })
  test('renders instruction to guess a word', () => {
    const instructions = findByTestAttr(wrapper, 'guess-instructions')
    expect(instructions.text().length).not.toBe(1)
  })
})

describe('if there are words guessed', () => {
  const guessedWords = [
    {guessedWord: 'train', letterMatchCount: 3},
    {guessedWord: 'agile', letterMatchCount: 1},
    {guessedWord: 'party', letterMatchCount: 5}
  ]
  let wrapper
  beforeEach(() => {
    wrapper = setUp({guessedWords})
  })
  test('renders without error', () => {
    const component = findByTestAttr(wrapper, 'component-guessed-words')
    expect(component.length).toBe(1)
  })
  test('renders "guessed words" section', () => {
    const guessedWordsNode = findByTestAttr(wrapper, 'guessed-words')
    expect(guessedWordsNode.length).toBe(1)
  })
  test('correct number of guessed words', () => {
    const guessedWordsNodes = findByTestAttr(wrapper, 'guessed-word')
    expect(guessedWordsNodes.length).toBe(guessedWords.length)
  })
})

Normal component with props #2

GuessedWordsMessage.js

import React from 'react'
import PropTypes from 'prop-types'

class GuessedWordMessage extends React.Component {
  render() {

    const message = this.props.guessedWords.length > 0
      ? <div>
          <p data-test="guess-word-message">
            Words guessed {this.props.guessedWords.length}
          </p>
        </div>
      : null

    return (
      message
    )
  }
}

GuessedWordMessage.propTypes = {
  guessedWords: PropTypes.arrayOf(
    PropTypes.shape({
      guessedWord: PropTypes.string.isRequired,
      letterMatchCount: PropTypes.number.isRequired
    })
  ).isRequired
}


export default GuessedWordMessage

GuessedWordsMessage.test.js

import React from 'react'
import {shallow} from 'enzyme'
import GuessedWordMessage from './GuessedWordMessage'
import {findByTestAttr} from '../test/testUtils'

const defaultProps = {guessedWords: []}


const setUp = (props = {}) => {
  const setUpProps = {...defaultProps, ...props}
  const wrapper = shallow(<GuessedWordMessage {...setUpProps} />)
  wrapper.debug()
  return wrapper
}

setUp()

describe('if there are no words guessed', () => {
  let wrapper

  beforeEach(() => {
    wrapper = setUp()
  })

  test('does not render', () => {
    const component = findByTestAttr(wrapper,'guess-word-message')
    expect(component.length).toBe(0)
  })
})

describe('if there are words guessed', () => {
  let wrapper

  beforeEach(() => {
    wrapper = setUp({guessedWords: [{guessedWord: 'bycycle', letterMatchCount: 2}]})
  })

  test('does not render', () => {
    const component = findByTestAttr(wrapper,'guess-word-message')
    expect(component.length).toBe(1)
  })
})

Normal component with props #3

Congrats.js

import React from 'react'
import PropTypes from 'prop-types'

/**
 * @function
 * @param {object} props
 * @returns {JSX.element}
 */
const Congrats = (props) => {
  if (props.success) {
    return (
      <div data-test="component-congrats" className="alert alert-success">
        <span data-test="congrats-message">
          Congratulations! you guessed the word!
        </span>
      </div>
    )
  } else {
    return (
      <div data-test="component-congrats">

      </div>
    )
  }
}

Congrats.propTypes = {
  success: PropTypes.bool.isRequired
}

export default  Congrats

Congrats.test.js

import React from 'react'
import {shallow} from 'enzyme'
import {checkProps, findByTestAttr} from '../test/testUtils'
import Congrats from './Congrats'

const defaultProps = {success: false}

/**
 * @function
 * @param {object}props
 * @returns {ShallowWrapper}
 */
const setup = (props = {}) => {
  const setUpProps = {...defaultProps, ...props}
  return shallow(<Congrats {...setUpProps} />)
}

test('render without error', () => {
  const wrapper = setup({success: false})
  const component = findByTestAttr(wrapper, 'component-congrats')
  expect(component.length).toBe(1)
})

test('renders no text when success prop is false', () => {
  const wrapper = setup({success: false})
  const component = findByTestAttr(wrapper, 'component-congrats')
  expect(component.text()).toBe('')

})

test('renders non-empty congrats message when succe', () => {
  const wrapper = setup({success: true})
  const message = findByTestAttr(wrapper, 'congrats-message')
  expect(message.text().length).not.toBe(0)
})

test('does not throw a warning with expected props', () => {
  const expectedProps = {success: false}
  const propError = checkProps(Congrats, expectedProps)
})

Normal component with props #4

App.js

import React, {Component} from 'react'
import logo from './logo.svg';
import './App.css';
import {connect} from 'react-redux'
import GuessedWords from './GuessedWords'
import Congrats from './Congrats'
import {getSecretWord} from './actions'
import Input from './Input'

export class UnconnectedApp extends Component {

  componentDidMount() {
    //get the secret word
    this.props.getSecretWord()
  }

  render() {
    return (
      <div className="container">
        <h1>Jotto</h1>
        <Congrats success={this.props.success} />
        <Input />
        <GuessedWords guessedWords={this.props.guessedWords}/>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  const {success, guessedWords, secretWord} = state
  return {
    success,
    guessedWords,
    secretWord
  }
}

const mapDispatchToProps = (dispatch, state) => {
  return {
    getSecretWord
  }
}


export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedApp);


App.test.js

import React from 'react'
import {storeFactory} from '../test/testUtils'
import {shallow} from 'enzyme'
import App, {UnconnectedApp} from './App'

const setup = (initialState = {}) => {
  const store = storeFactory(initialState)
  return shallow(<App store={store}/>).dive().dive()
}

describe('redux props', () => {
  test('has access to `success` state', () => {
    const success = true
    const wrapper = setup({success})
    const successProp = wrapper.instance().props.success
    expect(successProp).toBe(success)
  })
  test('has access to `secret word` state', () => {
    const secretWord = 'party'
    const wrapper = setup({secretWord})
    const secretWordProp = wrapper.instance().props.secretWord
    expect(secretWordProp).toBe(secretWord)
  })
  test('has access to `guessedWords` state', () => {
    const guessedWords = [{guessedWord: 'train', letterMatchCount: 3}]
    const wrapper = setup({guessedWords})
    const guessedWordsProp = wrapper.instance().props.guessedWords
    expect(guessedWords).toEqual(guessedWordsProp)
  })
  test('`getSecretWord` action creator is a function on the props', () => {
    const wrapper = setup()
    const getSecretWordProp = wrapper.instance().props.getSecretWord
    expect(getSecretWordProp).toBeInstanceOf(Function)
  })
})

test('`getSecretWord` runs on app mount', () => {
  const getSecretWordMock = jest.fn()

  const props = {success: false, getSecretWord: getSecretWordMock, guessedWords: []}

  //set up app component with getSecretWordMock as the getSecretWord prop
  const wrapper = shallow(<UnconnectedApp {...props} />)

  //run lifecycle method
  wrapper.instance().componentDidMount()

  //check to see if mock ran
  const getSecretWordCallCount = getSecretWordMock.mock.calls.length

  expect(getSecretWordCallCount).toBe(1)
});


Integration tests

integration.test.js

import {storeFactory} from '../test/testUtils'
import {guessWord, reset} from './actions'

describe('guessWord action dispatcher', () => {

  const secretWord = 'party'
  const unsuccessfulGuess = 'train'

  describe('no guessed words', () => {

    let store
    const initialState = {secretWord}

    beforeEach(() => {
      store = storeFactory(initialState)
    })

    test('updates state correctly for unsuccessful guess', () => {
      store.dispatch(guessWord(unsuccessfulGuess))
      const newState = store.getState()
      const expectedState = {
        ...initialState,
        success: false,
        serverError: false,
        guessedWords: [{
          guessedWord: unsuccessfulGuess,
          letterMatchCount: 3
        }]
      }
      expect(newState).toEqual(expectedState)
    })

    test('updates state correctly for successful guess', () => {
      store.dispatch(guessWord(secretWord))
      const newState = store.getState()
      const expectedState = {
        secretWord,
        success: true,
        serverError: false,
        guessedWords: [{
          guessedWord: secretWord,
          letterMatchCount: 5
        }]
      }
      expect(newState).toEqual(expectedState)
    })

  })
  describe('some guessed words', () => {
    const guessedWords = [ { guessedWord: 'agile', letterMatchCount: 1} ]
    const initialState = {guessedWords, secretWord}
    let store
    beforeEach(() => {
      store = storeFactory(initialState)
    })
    test('updates state correctly for unsuccessful guess', () => {
      store.dispatch(guessWord(unsuccessfulGuess))
      const newState = store.getState()
      const expectedState = {
        secretWord,
        success: false,
        serverError: false,
        guessedWords: [
          ...guessedWords,
          { guessedWord: unsuccessfulGuess, letterMatchCount: 3}
        ]
      }
      expect(newState).toEqual(expectedState)
    })
    test('updates state correctly for successful guess', () => {
      store.dispatch(guessWord(secretWord))
      const newState = store.getState()
      const expectedState = {
        secretWord,
        success: true,
        serverError: false,
        guessedWords: [
          ...guessedWords,
          {guessedWord: secretWord, letterMatchCount: 5}
        ]
      }
      expect(newState).toEqual(expectedState)
    })
    test('resets state after rest action has been dispatched', () => {
      store.dispatch(reset())
      const newState = store.getState()
      const expectedState = {
        secretWord,
        success: false,
        serverError: false,
        guessedWords: []
      }
      expect(newState).toEqual(expectedState)
    })
  })
})

redux actions

Example #1

index.js

import {getLetterMatchCount} from '../helpers'
import axios from 'axios'

export const actionTypes = {
  CORRECT_GUESS: 'CORRECT_GUESS',
  GUESS_WORD: 'GUESS_WORD',
  SET_SECRET_WORD: 'SET_SECRET_WORD',
  RESET: 'RESET',
  SERVER_ERROR: 'SERVER_ERROR'
}

export const guessWord = (guessedWord) => {
  return function (dispatch, getState) {

    const secretWord = getState().secretWord
    const letterMatchCount = getLetterMatchCount(guessedWord, secretWord)

    dispatch({
      type: actionTypes.GUESS_WORD,
      payload: {guessedWord, letterMatchCount}
    })

    if (guessedWord === secretWord) {
      dispatch({
        type: actionTypes.CORRECT_GUESS
      })
    }

  }
}

export const getSecretWord = () => {
  return (dispatch) => {
    return axios.get('http://localhost:3030')
      .then(response => {
        dispatch({
          type: actionTypes.SET_SECRET_WORD,
          payload: response.data
        })
      })
      .catch(error => {
        dispatch({type: actionTypes.SERVER_ERROR})
      })
  }
}

export const reset = () => {
  return (dispatch) => {
    dispatch({
      type: actionTypes.RESET
    })
  }
}

index.test.js

import moxios from 'moxios'

import {storeFactory} from '../../test/testUtils'
import {getSecretWord} from './index'

describe('secretWord action creator', () => {
  beforeEach(() => {
    moxios.install()
  })
  afterEach(() => {
    moxios.uninstall()
  })
  let store
  test('adds response word to state', () => {
    const secretWord = 'party'
    const store = storeFactory()

    moxios.wait(() => {
      const request = moxios.requests.mostRecent()
      request.respondWith({
        status: 200,
        response: secretWord
      })
    })

    return store.dispatch(getSecretWord())
      .then(() => {
        const newState = store.getState()
        expect(newState.secretWord).toBe(secretWord)
      })
  })
  test('updates error correctly after 404 error', () => {
    const store = storeFactory()

    moxios.wait(() => {
      const request = moxios.requests.mostRecent()
      request.respondWith({
        status: 404
      })
    })

    return store.dispatch(getSecretWord())
      .then(() => {
        const newState = store.getState()
        expect(newState.serverError).toBe(true)
      })
  })
})

Reducers

Example #1

serverReducer.js

import {actionTypes} from '../actions'

export default (state = false, action) => {
  switch (action.type) {
    case actionTypes.CORRECT_GUESS:
      return true
    default:
      return state
  }
}

serverReducer.test.js

import {actionTypes} from '../actions'
import successReducer from './successReducer'

test('returns default initial state of `false` when no action is passed', () => {
  const newState = successReducer(undefined, {})
  expect(newState).toBe(false)
})

test('returns state of true upon receiving an action of type `CORRECT_GUESS`', () => {
  const newState = successReducer(undefined, {type: actionTypes.CORRECT_GUESS})
  expect(newState).toBe(true)
})

Example #2

serverErrorReducer.js

import {actionTypes} from '../actions'

export default (state = false, action) => {
  switch (action.type) {
    case actionTypes.SERVER_ERROR:
      return true
    default:
      return state
  }
}

serverErrorReducer.test.js

import {actionTypes} from '../actions'
import serverErrorReducer from './serverErrorReducer'

test('returns false when initially created', () => {
  const newState = serverErrorReducer(undefined, {})
  expect(newState).toBe(false)
})

test('returns true when action server error passed', () => {
  const newState = serverErrorReducer(undefined, {type: actionTypes.SERVER_ERROR})
  expect(newState).toBe(true)
})

Helpers

Example #1

index.js

export function getLetterMatchCount(guessedWord, secretWord) {
  const secretLetterSet = new Set(secretWord.split(''))
  const guessedLetterSet = new Set(guessedWord.split(''))
  return [...secretLetterSet].filter(letter => guessedLetterSet.has(letter)).length
}

index.test.js

import {getLetterMatchCount} from './index'

describe('getLetterMatchCount', () => {
  const secretWord = 'party'
  test('returns correct count when there are no matching letters', () => {
    const letterMatchCount = getLetterMatchCount('bones', secretWord)
    expect(letterMatchCount).toBe(0)
  })
  test('returns correct count when there are 3 matching letters', () => {
    const letterMatchCount = getLetterMatchCount('train', secretWord)
    expect(letterMatchCount).toBe(3)
  })
  test('returns correct count when there are duplicate letters in the guess', () => {
    const letterMatchCount = getLetterMatchCount('parka', secretWord)
    expect(letterMatchCount).toBe(3)
  })
})

Extras

Moxios

integration.test.js

import React from 'react'
import {mount} from 'enzyme'
import moxios from 'moxios'
import Root from 'Root'
import App from 'components/App'
import {createMemoryHistory} from "history";

beforeEach(() => {
  moxios.install()
  moxios.stubRequest('http://jsonplaceholder.typicode.com/comments', {
    status: 200,
    response: Array(5).fill({name: 'Comment'})
  })
})

afterEach(() => {
  moxios.uninstall()
})

it('can fetch a list of comments and display them', (done) => {
  const wrapped = mount(
    <Root initialRoute={['/post']}>
      <App />
    </Root>
  )
  wrapped.find('li').at(1).simulate('click')
  wrapped.find('.fetch-comments').simulate('click')
  wrapped.find('li').at(0).simulate('click')

  // introduce a tiny little pause
  moxios.wait(() => {
    wrapped.update()
    expect(wrapped.find('li').length).toEqual(3)
    wrapped.unmount()
    done()
  })
})