ember-frost-test
v4.0.2
Published
The place for all the frosty testing things
Downloads
206
Readme
ember-frost-test
This repo serves as the home for tools and conventions used in testing the frost ecosystem.
Installation
ember install ember-frost-test
Testing Tools
We are using the following tools:
- ember-cli-mocha - This installs the Mocha testing framework.
- ember-cli-chai - This installs the Chai assertion library.
- ember-hook - This is a tool we use to create a separation between the DOM and our items under test.
- ember-sinon - This installs Sinon our method spying/stubbing/mocking tool.
- ember-test-utils - This provides our linting as well as test helpers that can be used to help test frost components.
- chai-jquery - This is a chai extension that provides assertions for jQuery.
- 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
orAcceptance
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.
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.
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 ..."
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
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
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:
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:
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:
@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:
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:
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:
expect(condition).to.eql(value) or expect(condition).to.equal(value)
instead of:
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.
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:
...
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:
...
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
Setup
git clone [email protected]:ciena-frost/ember-frost-test.git
cd ember-frost-list
npm install && bower install
Running
ember serve
- Visit your app at http://localhost:4200.
Running Tests
npm test
(Runsember 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/.