This repo serves as the home for tools and conventions used in testing the frost ecosystem.
ember install ember-frost-test
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.
For any unfamiliar with the BDD style describe
/beforeEach
/it
, here's an overview of how one should
organize a test module.
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
.
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.
Additional describe
blocks nested within the top-level describe
serve one of two purposes, defining/declaring a
scope, or defining a use-case.
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
})
})
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
})
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.
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)
})
})
})
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')
})
})
})
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.
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
})
})
})
Updates to the tools and/or conventions used in ember-frost-test can be submitted for discussion via the RFC process
git clone [email protected]:ciena-frost/ember-frost-test.git
cd ember-frost-list
npm install && bower install
ember serve
- Visit your app at http://localhost:4200.
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
ember build
For more information on using ember-cli, visit http://ember-cli.com/.