This documentation is for the old Kea 0.28. To see the latest docs, click here!

Testing logic stores

NOTE! This section of the guide is still heavily under construction! Please check back in a few days.

Generic test setup

For testing kea we recommend using jest.

Resetting the cache before each test

// logic.test.js
import { resetKeaCache } from 'kea'

beforeEach(() => {
  resetKeaCache()
})

test('starts from a clear state', () => {
  // ...
})

Creating a store for your tests

import { resetKeaCache, keaReducer } from 'kea'
import { keaSaga } from 'kea-saga'
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import createSagaMiddleware from 'redux-saga'

function getStore () {
  resetKeaCache()

  const reducers = combineReducers({
    scenes: keaReducer('scenes')
  })

  const sagaMiddleware = createSagaMiddleware()
  const finalCreateStore = compose(
    applyMiddleware(sagaMiddleware)
  )(createStore)

  const store = finalCreateStore(reducers)

  sagaMiddleware.run(keaSaga)

  return store
}

test('do something with the store', () => {
  const store = getStore()

  // ...
})

Testing logic

/* global expect, test */
import { keaReducer } from 'kea'
import PropTypes from 'prop-types'

import getStore from './helpers/get-store'

test('homepage logic has all the right properties', () => {
  const store = getStore()

  // we must require because importing makes
  const logic = require('./logic').default

  expect(logic.path).toEqual(['scenes', 'homepage', 'index'])

  // actions
  expect(Object.keys(logic.actions)).toEqual(['updateName'])

  const { updateName } = logic.actions

  expect(typeof updateName).toBe('function')
  expect(updateName.toString()).toBe('update name (homepage.index)')
  expect(updateName('newname')).toEqual({ payload: { name: 'newname' }, type: updateName.toString() })

  // reducers
  const defaultValues = { name: 'Chirpy' }
  const state = { scenes: { homepage: { index: defaultValues } } }
  expect(Object.keys(logic.reducers).sort()).toEqual(['capitalizedName', 'name'])

  expect(logic.reducers).toHaveProperty('name.reducer')
  expect(logic.reducers).toHaveProperty('name.type', PropTypes.string)
  expect(logic.reducers).toHaveProperty('name.value', 'Chirpy')

  const nameReducer = logic.reducers.name.reducer
  expect(Object.keys(nameReducer)).toEqual([ updateName.toString() ])
  expect(nameReducer[updateName.toString()]).toBeDefined()
  expect(nameReducer[updateName.toString()]('', { name: 'newName' })).toBe('newName')

  expect(logic.reducers).not.toHaveProperty('capitalizedName.reducer')
  expect(logic.reducers).toHaveProperty('capitalizedName.type', PropTypes.string)
  expect(logic.reducers).not.toHaveProperty('capitalizedName.value', 'chirpy')

  // big reducer
  expect(typeof logic.reducer).toBe('function')
  expect(logic.reducer({}, { type: 'random action' })).toEqual(defaultValues)
  expect(logic.reducer({ name: 'something' }, { type: 'random action' })).toEqual({ name: 'something' })
  expect(logic.reducer({ name: 'something' }, updateName('newName'))).toEqual({ name: 'newName' })

  // selectors
  expect(Object.keys(logic.selectors).sort()).toEqual(['capitalizedName', 'name', 'root'])
  expect(logic.selectors.name(state)).toEqual('Chirpy')
  expect(logic.selectors.capitalizedName(state)).toEqual('Chirpy')

  // root selector
  expect(logic.selector(state)).toEqual(defaultValues)
  expect(logic.selectors.root(state)).toEqual(defaultValues)
})

Testing components with enzyme

/* global test, expect */
import './helper/jsdom'

import React, { Component } from 'react'
import { kea } from 'kea'
import PropTypes from 'prop-types'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'

import getStore from './helper/get-store'

class SampleComponent extends Component {
  render () {
    const { id, name, capitalizedName } = this.props
    const { updateName } = this.actions

    return (
      <div>
        <div className='id'>{id}</div>
        <div className='name'>{name}</div>
        <div className='capitalizedName'>{capitalizedName}</div>
        <div className='updateName' onClick={updateName}>updateName</div>
      </div>
    )
  }
}

test('connects to react components', () => {
  const store = getStore()

  const dynamicLogic = kea({
    key: (props) => props.id,
    path: (key) => ['scenes', 'something', key],
    actions: ({ constants }) => ({
      updateName: name => ({ name })
    }),
    reducers: ({ actions, constants }) => ({
      name: ['chirpy', PropTypes.string, {
        [actions.updateName]: (state, payload) => payload.name + payload.key
      }]
    }),
    selectors: ({ constants, selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name.trim().split(' ').map(k => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`).join(' ')
        },
        PropTypes.string
      ]
    })
  })

  const ConnectedComponent = dynamicLogic(SampleComponent)

  const wrapper = mount(
    <Provider store={store}>
      <ConnectedComponent id={12} />
    </Provider>
  )

  expect(wrapper.find('.id').text()).toEqual('12')
  expect(wrapper.find('.name').text()).toEqual('chirpy')
  expect(wrapper.find('.capitalizedName').text()).toEqual('Chirpy')

  // for now we must dispatch a discard action to regenerate the store
  store.dispatch({ type: 'discard' })
  expect(store.getState()).toEqual({ scenes: { something: { 12: { name: 'chirpy' } } } })

  const sampleComponent = wrapper.find('SampleComponent').node

  expect(sampleComponent.actions).toBeDefined()
  expect(Object.keys(sampleComponent.actions)).toEqual(['updateName'])

  const { updateName } = sampleComponent.actions
  updateName('somename')

  expect(store.getState()).toEqual({ scenes: { something: { 12: { name: 'somename12' } } } })

  wrapper.render()

  expect(wrapper.find('.id').text()).toEqual('12')
  expect(wrapper.find('.name').text()).toEqual('somename12')
  expect(wrapper.find('.capitalizedName').text()).toEqual('Somename12')
})

Other resources

Check out the source documentation on testing redux, selectors and sagas.