-
Notifications
You must be signed in to change notification settings - Fork 142
[docs] Add testing section #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
--- | ||
title: "Testing" | ||
section: "Guide" | ||
--- | ||
|
||
# Testing | ||
|
||
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 | ||
is that a particular action or set of actions properly transforms the Reactor from **State A** to **State B**. | ||
|
||
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**. | ||
|
||
## Example | ||
|
||
In our testing example we will test our Project module while contains two stores, the `currentProjectIdStore` and the `projectStore` as well as | ||
actions and getters. | ||
|
||
#### `index.js` | ||
|
||
```javascript | ||
import reactor from '../reactor' | ||
import projectStore from './stores/projectStore' | ||
import currentProjectIdStore from './stores/currentProjectIdStore' | ||
import actions from './actions' | ||
import getters from './getters' | ||
|
||
reactor.registerStores({ | ||
currentProjectId: currentProjectIdStore, | ||
projects: projectStore, | ||
}) | ||
|
||
export default { getters, actions } | ||
``` | ||
|
||
#### `stores/currentProjectIdStore.js` | ||
|
||
```javascript | ||
import { Store } from 'nuclear-js' | ||
import { CHANGE_CURRENT_PROJECT_ID } from '../actionTypes' | ||
|
||
export default Store({ | ||
getInitialState() { | ||
return null | ||
}, | ||
|
||
initialize() { | ||
this.on(CHANGE_CURRENT_PROJECT_ID, (currentId, newId) => newId) | ||
}, | ||
}) | ||
``` | ||
|
||
#### `stores/projectStore.js` | ||
|
||
```javascript | ||
import { Store, toImmutable } from 'nuclear-js' | ||
import { LOAD_PROJECTS } from '../actionTypes' | ||
|
||
export default Store({ | ||
getInitialState() { | ||
// will maintain a map of project id => project object | ||
return toImmutable({}) | ||
}, | ||
|
||
initialize() { | ||
this.on(LOAD_PROJECTS, loadProjects) | ||
}, | ||
}) | ||
|
||
/** | ||
* @param {Immutable.Map} state | ||
* @param {Object} payload | ||
* @param {Object[]} payload.data | ||
* @return {Immutable.Map} state | ||
*/ | ||
function loadProjects(state, payload) { | ||
return state.withMutations(state => { | ||
payload.forEach(function(project) { | ||
state.set(project.id, project) | ||
}) | ||
}) | ||
} | ||
``` | ||
|
||
#### `actions.js` | ||
|
||
```javascript | ||
import Api from '../utils/api' | ||
import reactor from '../reactor' | ||
import { LOAD_PROJECTS } from './actionTypes' | ||
|
||
export default { | ||
fetchProjects() { | ||
return Api.fetchProjects.then(projects => { | ||
reactor.dispatch(LOAD_PROJECTS, { | ||
data: projects, | ||
}) | ||
}) | ||
}, | ||
|
||
/** | ||
* @param {String} id | ||
*/ | ||
setCurrentProjectId(id) { | ||
reactor.dispatch(CHANGE_CURRENT_PROJECT_ID, id) | ||
}, | ||
} | ||
``` | ||
|
||
#### `getters.js` | ||
|
||
``` | ||
cosnt projectsMap = ['projects'] | ||
|
||
const currentProjectId = ['currentProjectId'] | ||
|
||
const currentProject = [ | ||
currentProjectId, | ||
projectsMap, | ||
(id, projects) => projects.get(id) | ||
] | ||
|
||
export default { projectsMap, currentProject, currentProjectId } | ||
``` | ||
|
||
### Tests | ||
|
||
Given our module we want to test the following: | ||
|
||
- Using `actions.setCurrentProjectId()` sets the correct id using the `currentProjectId` getter | ||
|
||
- When `Api.fetchProducts` is stubbed with mock data, calling `actions.fetchProjects` properly poulates | ||
the projects store by using the `projectsMap` getter | ||
|
||
- When projects have been loaded and currentProjectId set, `currentProject` getter works | ||
|
||
**Testing Tools** | ||
|
||
We will use the following tools: mocha, sinon, expect.js. The same testing ideas can be implemented with a variety of tools. | ||
|
||
#### `tests.js` | ||
|
||
```javascript | ||
import reactor from '../reactor' | ||
import Api from '../utils/api' | ||
import expect from 'expect' | ||
|
||
// module under test | ||
import Project from './index' | ||
|
||
let mockProjects = [ | ||
{ id: '123-abc', name: 'project 1' }, | ||
{ id: '456-cdf', name: 'project 2' }, | ||
] | ||
|
||
describe('modules/Project', () => { | ||
afterEach(() => { | ||
flux.reset() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You keep referring to flux but it is never defined. You mean reactor? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch @balloob - |
||
}) | ||
|
||
describe('actions', () => { | ||
describe('#setCurrentProjectId', () => { | ||
it("should set the current project id", () => { | ||
Project.actions.setCurrentProjectId('123-abc') | ||
|
||
expect(flux.evaluate(Project.getters.currentProjectId)).to.be('123-abc') | ||
}) | ||
}) | ||
|
||
describe('#fetchProjects', () => { | ||
beforeEach(() => { | ||
let fetchProjectsPromise = new Promise((resolve, reject) => { | ||
resolve(mockProjects) | ||
}) | ||
|
||
sinon.stub(Api, 'fetchProjects').returns(fetchProjectsPromise) | ||
}) | ||
|
||
afterEach(() => { | ||
Api.fetchProjects.restore() | ||
}) | ||
|
||
it('should load projects into the project store', (done) => { | ||
Project.actions.fetchProjects().then(() => { | ||
projectsMap = flux.evaluateToJS(Project.getters.projectMap) | ||
expect(projectsMap).to.eql({ | ||
'123-abc': { id: '123-abc', name: 'project 1' }, | ||
'456-cdf': { id: '456-cdf', name: 'project 2' }, | ||
}) | ||
done() | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('getters', () => { | ||
describe('#currentProject', () => { | ||
beforeEach((done) => { | ||
let fetchProjectsPromise = new Promise((resolve, reject) => { | ||
resolve(mockProjects) | ||
}) | ||
sinon.stub(Api, 'fetchProjects').returns(fetchProjectsPromise) | ||
|
||
// wait for the projects to be fetched / loaded into store before test | ||
Project.actions.fetchProjects().then(() => { | ||
done() | ||
}) | ||
}) | ||
|
||
afterEach(() => { | ||
Api.fetchProjects.restore() | ||
}) | ||
|
||
it('should evaluate to the current project when the currentProjectId is set', () => { | ||
expect(flux.evaluate(Project.getters.currentProject)).to.be(undefined) | ||
|
||
Project.actions.setCurrentProjectId('123-abc') | ||
|
||
expect(flux.evaluateToJS(Project.getters.currentProject)).to.eql({ | ||
id: '123-abc', | ||
name: 'project 1', | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
``` | ||
|
||
## Recap | ||
|
||
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 | ||
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 | ||
for all data flow and state logic. | ||
|
||
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, | ||
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 | ||
we are able to keep the test high level with limited stubs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo, poulates
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in #144.