Skip to content

Commit 03b4743

Browse files
committed
Merge pull request #143 from optimizely/testing-docs
[docs] Add testing section
2 parents b5d89fa + af495fd commit 03b4743

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

docs/src/docs/08-testing.md

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)