|
| 1 | +--- |
| 2 | +title: "Testing" |
| 3 | +section: "Guide" |
| 4 | +--- |
| 5 | + |
| 6 | +# Testing |
| 7 | + |
| 8 | +The most valuable and easy to write tests for NuclearJS are unit tests. **The unit in Nuclear is the action.** The key assertion we want to make |
| 9 | +is that a particular action or set of actions properly transforms the Reactor from **State A** to **State B**. |
| 10 | + |
| 11 | +This is done by setting up the reactor with the proper state, using actions, executing the action under test and asserting proper state via **Getters**. |
| 12 | + |
| 13 | +## Example |
| 14 | + |
| 15 | +In our testing example we will test our Project module while contains two stores, the `currentProjectIdStore` and the `projectStore` as well as |
| 16 | +actions and getters. |
| 17 | + |
| 18 | +#### `index.js` |
| 19 | + |
| 20 | +```javascript |
| 21 | +import reactor from '../reactor' |
| 22 | +import projectStore from './stores/projectStore' |
| 23 | +import currentProjectIdStore from './stores/currentProjectIdStore' |
| 24 | +import actions from './actions' |
| 25 | +import getters from './getters' |
| 26 | + |
| 27 | +reactor.registerStores({ |
| 28 | + currentProjectId: currentProjectIdStore, |
| 29 | + projects: projectStore, |
| 30 | +}) |
| 31 | + |
| 32 | +export default { getters, actions } |
| 33 | +``` |
| 34 | + |
| 35 | +#### `stores/currentProjectIdStore.js` |
| 36 | + |
| 37 | +```javascript |
| 38 | +import { Store } from 'nuclear-js' |
| 39 | +import { CHANGE_CURRENT_PROJECT_ID } from '../actionTypes' |
| 40 | + |
| 41 | +export default Store({ |
| 42 | + getInitialState() { |
| 43 | + return null |
| 44 | + }, |
| 45 | + |
| 46 | + initialize() { |
| 47 | + this.on(CHANGE_CURRENT_PROJECT_ID, (currentId, newId) => newId) |
| 48 | + }, |
| 49 | +}) |
| 50 | +``` |
| 51 | + |
| 52 | +#### `stores/projectStore.js` |
| 53 | + |
| 54 | +```javascript |
| 55 | +import { Store, toImmutable } from 'nuclear-js' |
| 56 | +import { LOAD_PROJECTS } from '../actionTypes' |
| 57 | + |
| 58 | +export default Store({ |
| 59 | + getInitialState() { |
| 60 | + // will maintain a map of project id => project object |
| 61 | + return toImmutable({}) |
| 62 | + }, |
| 63 | + |
| 64 | + initialize() { |
| 65 | + this.on(LOAD_PROJECTS, loadProjects) |
| 66 | + }, |
| 67 | +}) |
| 68 | + |
| 69 | +/** |
| 70 | + * @param {Immutable.Map} state |
| 71 | + * @param {Object} payload |
| 72 | + * @param {Object[]} payload.data |
| 73 | + * @return {Immutable.Map} state |
| 74 | + */ |
| 75 | +function loadProjects(state, payload) { |
| 76 | + return state.withMutations(state => { |
| 77 | + payload.forEach(function(project) { |
| 78 | + state.set(project.id, project) |
| 79 | + }) |
| 80 | + }) |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +#### `actions.js` |
| 85 | + |
| 86 | +```javascript |
| 87 | +import Api from '../utils/api' |
| 88 | +import reactor from '../reactor' |
| 89 | +import { LOAD_PROJECTS } from './actionTypes' |
| 90 | + |
| 91 | +export default { |
| 92 | + fetchProjects() { |
| 93 | + return Api.fetchProjects.then(projects => { |
| 94 | + reactor.dispatch(LOAD_PROJECTS, { |
| 95 | + data: projects, |
| 96 | + }) |
| 97 | + }) |
| 98 | + }, |
| 99 | + |
| 100 | + /** |
| 101 | + * @param {String} id |
| 102 | + */ |
| 103 | + setCurrentProjectId(id) { |
| 104 | + reactor.dispatch(CHANGE_CURRENT_PROJECT_ID, id) |
| 105 | + }, |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +#### `getters.js` |
| 110 | + |
| 111 | +``` |
| 112 | +cosnt projectsMap = ['projects'] |
| 113 | +
|
| 114 | +const currentProjectId = ['currentProjectId'] |
| 115 | +
|
| 116 | +const currentProject = [ |
| 117 | + currentProjectId, |
| 118 | + projectsMap, |
| 119 | + (id, projects) => projects.get(id) |
| 120 | +] |
| 121 | +
|
| 122 | +export default { projectsMap, currentProject, currentProjectId } |
| 123 | +``` |
| 124 | + |
| 125 | +### Tests |
| 126 | + |
| 127 | +Given our module we want to test the following: |
| 128 | + |
| 129 | +- Using `actions.setCurrentProjectId()` sets the correct id using the `currentProjectId` getter |
| 130 | + |
| 131 | +- When `Api.fetchProducts` is stubbed with mock data, calling `actions.fetchProjects` properly poulates |
| 132 | + the projects store by using the `projectsMap` getter |
| 133 | + |
| 134 | +- When projects have been loaded and currentProjectId set, `currentProject` getter works |
| 135 | + |
| 136 | +**Testing Tools** |
| 137 | + |
| 138 | +We will use the following tools: mocha, sinon, expect.js. The same testing ideas can be implemented with a variety of tools. |
| 139 | + |
| 140 | +#### `tests.js` |
| 141 | + |
| 142 | +```javascript |
| 143 | +import reactor from '../reactor' |
| 144 | +import Api from '../utils/api' |
| 145 | +import expect from 'expect' |
| 146 | + |
| 147 | +// module under test |
| 148 | +import Project from './index' |
| 149 | + |
| 150 | +let mockProjects = [ |
| 151 | + { id: '123-abc', name: 'project 1' }, |
| 152 | + { id: '456-cdf', name: 'project 2' }, |
| 153 | +] |
| 154 | + |
| 155 | +describe('modules/Project', () => { |
| 156 | + afterEach(() => { |
| 157 | + flux.reset() |
| 158 | + }) |
| 159 | + |
| 160 | + describe('actions', () => { |
| 161 | + describe('#setCurrentProjectId', () => { |
| 162 | + it("should set the current project id", () => { |
| 163 | + Project.actions.setCurrentProjectId('123-abc') |
| 164 | + |
| 165 | + expect(flux.evaluate(Project.getters.currentProjectId)).to.be('123-abc') |
| 166 | + }) |
| 167 | + }) |
| 168 | + |
| 169 | + describe('#fetchProjects', () => { |
| 170 | + beforeEach(() => { |
| 171 | + let fetchProjectsPromise = new Promise((resolve, reject) => { |
| 172 | + resolve(mockProjects) |
| 173 | + }) |
| 174 | + |
| 175 | + sinon.stub(Api, 'fetchProjects').returns(fetchProjectsPromise) |
| 176 | + }) |
| 177 | + |
| 178 | + afterEach(() => { |
| 179 | + Api.fetchProjects.restore() |
| 180 | + }) |
| 181 | + |
| 182 | + it('should load projects into the project store', (done) => { |
| 183 | + Project.actions.fetchProjects().then(() => { |
| 184 | + projectsMap = flux.evaluateToJS(Project.getters.projectMap) |
| 185 | + expect(projectsMap).to.eql({ |
| 186 | + '123-abc': { id: '123-abc', name: 'project 1' }, |
| 187 | + '456-cdf': { id: '456-cdf', name: 'project 2' }, |
| 188 | + }) |
| 189 | + done() |
| 190 | + }) |
| 191 | + }) |
| 192 | + }) |
| 193 | + }) |
| 194 | + |
| 195 | + describe('getters', () => { |
| 196 | + describe('#currentProject', () => { |
| 197 | + beforeEach((done) => { |
| 198 | + let fetchProjectsPromise = new Promise((resolve, reject) => { |
| 199 | + resolve(mockProjects) |
| 200 | + }) |
| 201 | + sinon.stub(Api, 'fetchProjects').returns(fetchProjectsPromise) |
| 202 | + |
| 203 | + // wait for the projects to be fetched / loaded into store before test |
| 204 | + Project.actions.fetchProjects().then(() => { |
| 205 | + done() |
| 206 | + }) |
| 207 | + }) |
| 208 | + |
| 209 | + afterEach(() => { |
| 210 | + Api.fetchProjects.restore() |
| 211 | + }) |
| 212 | + |
| 213 | + it('should evaluate to the current project when the currentProjectId is set', () => { |
| 214 | + expect(flux.evaluate(Project.getters.currentProject)).to.be(undefined) |
| 215 | + |
| 216 | + Project.actions.setCurrentProjectId('123-abc') |
| 217 | + |
| 218 | + expect(flux.evaluateToJS(Project.getters.currentProject)).to.eql({ |
| 219 | + id: '123-abc', |
| 220 | + name: 'project 1', |
| 221 | + }) |
| 222 | + }) |
| 223 | + }) |
| 224 | + }) |
| 225 | +}) |
| 226 | +``` |
| 227 | + |
| 228 | +## Recap |
| 229 | + |
| 230 | +When testing NuclearJS code it makes sense to test around actions asserting proper state updates via getters. While these tests may seem simple, they are |
| 231 | +testing that our stores, actions and getters are all working in a cohesive manner. As your codebase scales out these tests be the foundation of unit tests |
| 232 | +for all data flow and state logic. |
| 233 | + |
| 234 | +Another thing to note is that we did not stub or mock any part of the NuclearJS system. While testing in isolation is good for a variety of reasons, |
| 235 | +isolating too much will cause your tests to be unrealistic and more prone to breakage after refactoring. By testing the entire module as a unit |
| 236 | +we are able to keep the test high level with limited stubs. |
0 commit comments