ember-frost-test
Version:
The place for all the frosty testing things
430 lines (326 loc) • 11.7 kB
Markdown
# ember-frost-test
This repo serves as the home for tools and conventions used in testing the frost ecosystem.
[![Travis][ci-img]][ci-url] [![NPM][npm-img]][npm-url]
## Installation
```bash
ember install ember-frost-test
```
## Testing Tools
We are using the following tools:
* [ember-cli-mocha](https://github.com/ember-cli/ember-cli-mocha) - This installs the [Mocha](http://mochajs.org)
testing framework.
* [ember-cli-chai](https://github.com/ember-cli/ember-cli-chai) - This installs the [Chai](http://chaijs.com)
assertion library.
* [ember-hook](https://github.com/Ticketfly/ember-hook) - This is a tool we use to create a separation between the
DOM and our items under test.
* [ember-sinon](https://github.com/csantero/ember-sinon) - This installs [Sinon](http://sinonjs.org) our method
spying/stubbing/mocking tool.
* [ember-test-utils](https://github.com/ciena-blueplanet/ember-test-utils) - This provides our linting as well as
test helpers that can be used to help test frost components.
* [chai-jquery](https://github.com/chaijs/chai-jquery) - This is a chai extension that provides assertions for jQuery.
* [sinon-chai](http://chaijs.com/plugins/sinon-chai/) - This is a chai extension that provides assertions for sinon.js.
## Testing Conventions
### Organizing your tests
For any unfamiliar with the BDD style `describe`/`beforeEach`/`it`, here's an overview of how one should
organize a test module.
#### Top level `describe`
Each module should contain a single top-level `describe` which explains what it is that's being tested. We have
test helpers to streamline formatting the message for this top-level `describe` label, but the current format is:
```
<testType> / <moduleType> / <nameOfModule> /
```
* `testType` - `Unit`, `Integration` or `Acceptance`
* `moduleType` - `Component`, `Route`, `Controller`, etc.
* `nameOfModule` - `frost-text`, `things`, etc.
We use `/` as a delimiter instead of '|' because when using `?grep=` to scope your tests in the URL, `|` is treated
like an `or` operator. We include a trailing `/` so that when clicking on the test for `frost-select` you don't
also get a test for `frost-select-outlet`.
#### Top level `beforeEach`/`afterEach`
The top-level `beforeEach` can be used to setup anything that will be needed for every use-case being tested, for
example, creating the `sinon` sandbox or creating an instance of the thing under test. The `afterEach` should be
used to clean up things that need cleaning after each `it`, like restoring all the stubs/spies in the sandbox.
#### Nested `describe` blocks
Additional `describe` blocks nested within the top-level `describe` serve one of two purposes, defining/declaring a
scope, or defining a use-case.
##### Defining/declaring a scope
A `describe` that is just grouping a set of other `describe` blocks because they are similar, generally won't need
a `beforeEach` because there's nothing to set up.
```javascript
describe('Computed Properties', function () {
describe('foo', function () {
// actual tests for foo
})
describe('bar', function () {
// actual tests for bar
})
})
```
##### Defining a use-case
The second, more common use of a nested `describe` is to describe/define a specific use-case, a state of the system or
an action being performed. The label for these `describe` blocks will often start with "when".
These types of `describe` blocks should always include a `beforeEach` which actually sets up the described state of
the system or performs the described action.
```javascript
describe('when the "text" property is set', function () {
beforeEach(function () {
component.set('text', 'foo bar baz')
})
// expect something
})
describe('when the button is clicked', function () {
beforeEach(function () {
this.$('button').click()
})
// expect something
})
```
#### `it() blocks`
The `it()` blocks are used to describe an expected outcome. They generally start with "should", this is so it reads
like English, "it should ..."
```javascript
it('should add the "foo-bar" class to the input element', function () {
expect(this.$('input')).to.have.class('foo-bar')
})
```
You want to explain, in human-readable text, exactly what it is that's supposed to be happening, so that when the test
fails, a developer knows exactly what isn't working anymore.
As a rule-of-thumb, you should never be "doing something" in an `it()` the `it()` is for verifying the state of the
system. If you need to "do something" else before verifying, use a nested `describe` to describe what it is that
you are doing, and a `beforeEach` within that `describe` to actually do it.
### The role of acceptance/integration/unit tests
#### Acceptance tests are used to test user interaction and application flow
Some examples of what we would use an acceptance test for are:
Validating routes
```javascript
it('can visit /routeName', function (done) {
visit('/routeName')
return andThen(function () {
expect(currentPath()).to.equal('routeName.index')
})
})
```
Interacting with components/elements on a page to validate a behavior results in the expected outcome
```javascript
it('can create a user', function () {
visit('/users')
click(hook('createUserButton'))
return andThen(function () {
expect(hook('userRecord').length).to.equal(1)
})
})
```
#### Integration tests are great for validating the DOM structure and changes to the DOM structure that result from interaction with a component's different properties and actions.
DOM structure altered via interaction with component:
```javascript
describe('when disabled property is set', function () {
beforeEach(function () {
this.render(hbs`
{{frost-password
disabled=true
}}
`)
})
it('should set the "disabled" prop on the inner <input> element', function () {
expect(this.$('.frost-password input')).to.have.prop('disabled', true)
})
})
```
Validate interacting with component fires closure action:
```javascript
describe('when onClick property is set', function() {
let clickHandler
beforeEach(function() {
clickHandler = sinon.stub()
this.setProperties({clickHandler})
this.render(hbs`
{{frost-link 'title'
onClick=(action clickHandler)
}}
`)
})
describe('when the anchor tag is clicked', function() {
beforeEach(function() {
this.$('a').trigger('click')
})
it('should call the click handler', function() {
expect(clickHandler).to.have.callCount(1)
})
})
})
```
#### Unit tests are used to test "units" of functionality
Some examples of what we would use a unit test for are:
Validating computed properties, object methods and observers
Computed Property:
```javascript
@readOnly
@computed('icon', 'text')
/**
* Determine whether or not button is text only (no icon)
* @param {String} icon - button icon
* @param {String} text - button text
* @returns {Boolean} whether or not button is text only (no icon)
*/
isTextOnly (icon, text) {
return !isEmpty(text) && isEmpty(icon)
},
describe('"isTextOnly" computed property', function () {
describe('when only "text" is set', function () {
beforeEach(function() {
component.set('text', 'testText')
})
it('should be true', function() {
expect(component.get('isTextOnly')).to.equal(true)
})
})
describe('when both "icon" and "text" are set', function () {
beforeEach(function() {
component.setProperties({
icon: 'round-add'
text: 'testText',
})
})
it('should be false', function() {
expect(component.get('isTextOnly')).to.equal(false)
})
})
})
```
Object Method:
```javascript
checkSelectionValidity (selection) {
return typeOf(selection.onSelect) === 'function'
},
describe('checkSelectionValidity()', function () {
let selection, ret
describe('when selection is set properly', function () {
beforeEach(function() {
selection = {
onSelect() {}
}
ret = component.checkSelectionValidity(selection)
})
it('should be true', function () {
expect(ret).to.equal(true)
})
})
describe('when selection is missing "onSelect" function', function () {
beforeEach(function() {
selection = {}
ret = component.checkSelectionValidity(selection)
})
it('should be false', function () {
expect(ret).to.equal(false)
})
})
})
```
Observer:
```javascript
doSomething: Ember.observer('foo', function() {
this.set('other', 'yes');
})
describe('someThing', function () {
let someThing
beforeEach(function () {
someThing = this.subject()
})
describe('when foo changes', function () {
beforeEach(function() {
someThing.set('foo', 'baz')
})
it('should set "other" to "yes"', function () {
expect(someThing.get('other')).to.equal('yes')
})
})
})
```
### Use .to.eql() or .to.equal() instead of property based assertions
In our expect() we should use:
```javascript
expect(condition).to.eql(value) or expect(condition).to.equal(value)
```
instead of:
```javascript
expect(condition).to.be.ok
expect(condition).to.be.true
expect(condition).to.be.false
expect(condition).to.be.null
expect(condition).to.be.undefined
```
This is because property based assertions [are dangerous](https://github.com/chaijs/chai/issues/726).
### Use sinon.sandbox() for spying, stubbing, mocking methods.
Combined with `beforeEach()` and `afterEach()` we can easily create the sandbox before a test
and clean it up afterwards.
In an integration test:
```javascript
...
import sinon from 'sinon'
const test = integration('frost-whatever')
describe(test.label, function () {
test.setup()
let sandbox
beforeEach(function () {
sandbox = sinon.sandbox.create()
})
afterEach(function () {
sandbox.restore()
})
describe('when x happens', function () {
beforeEach(function () {
sandbox.spy(object, 'methodName')
// do x
})
it('should call methodName', function () {
expect(object.methodName).to.have.callCount(1)
})
})
})
```
In a unit test:
```javascript
...
import sinon from 'sinon'
describe('Unit / Mixin / FrostWhatever', function () {
let sandbox, subject
beforeEach(function () {
sandbox = sinon.sandbox.create()
subject = Controller.extend(FrostWhateverMixin).create()
})
afterEach(function () {
sandbox.restore()
})
describe('when stuff happens', function () {
beforeEach(function () {
sandbox.stub(object, 'method').returns({1: true})
// do stuff
})
it('should do some other stuff', function () {
// expect some other stuff to have happened
})
})
})
```
## Requesting Changes (RFCS)
Updates to the tools and/or conventions used in ember-frost-test can be submitted for discussion
via the [RFC process](https://github.com/ciena-frost/ember-frost-test/blob/master/rfcs/README.md)
### Setup
```bash
git clone git@github.com:ciena-frost/ember-frost-test.git
cd ember-frost-list
npm install && bower install
```
## Running
* `ember serve`
* Visit your app at [http://localhost:4200](http://localhost:4200).
## Running Tests
* `npm test` (Runs `ember try:each` to test your addon against multiple Ember versions)
* `ember test`
* `ember test --server`
## Building
* `ember build`
For more information on using ember-cli, visit [http://ember-cli.com/](http://ember-cli.com/).
[ci-img]: https://img.shields.io/travis/ciena-frost/ember-frost-test.svg "Travis CI Build Status"
[ci-url]: https://travis-ci.org/ciena-frost/ember-frost-test
[npm-img]: https://img.shields.io/npm/v/ember-frost-test.svg "NPM Version"
[npm-url]: https://www.npmjs.com/package/ember-frost-test