node package manager
Stop wasting time. Easily manage code sharing in your team. Create a free org »

ember-frost-test

ember-frost-test

This repo serves as the home for tools and conventions used in testing the frost ecosystem.

Travis NPM

Installation

ember install ember-frost-test

Testing Tools

We are using the following tools:

  • ember-cli-mocha - This is our testing framework. It also includes, chaijs our 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 is our method spying/stubbing/mocking tool.
  • ember-test-utils - These are our test helpers that can be used to help test frost components.

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.

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 git@github.com:ciena-frost/ember-frost-test.git
cd ember-frost-list
npm install && bower install

Running

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/.