The purpose of this style guide is to offer suggested best practices when writing Jasmine unit tests.
This style guide ultimately represents the opinions of its contributors. If you disagree with anything, or wish to add more, please create an issue or submit a pull request. Our goal is to continuously improve the guide and build consensus around it.
- Speak Human
- Write Unit Tests
- Arrange-Act-Assert
- Don't Repeat Yourself
this
Is How We Do It- Avoid the
All
s - Be
describe
tive - Write Minimum Passable Tests
Label your test suites (describe
blocks) and specs (it
blocks) in a way that clearly conveys the intention of each unit test. Note that the name of each test is the title of its it
preceded by all its parent describe
names. Favor assertive verbs and avoid ones like "should."
- Test suite becomes documentation for your codebase (helpful for new team members and non-technical stakeholders)
- Failure messages accurately depict what is broken
- Forces good naming conventions in tested code
// Output: "Array adds to the end"
describe('Array', function() {
it('adds to the end', function() {
var initialArray = [1];
initialArray.push(2);
expect(initialArray).toEqual([1, 2]);
});
});
// Output: "Array.prototype .push(x) appends x to the end of the Array"
describe('Array.prototype', function() {
describe('.push(x)', function() {
it('appends x to the end of the Array', function() {
var initialArray = [1];
initialArray.push(2);
expect(initialArray).toEqual([1, 2]);
});
});
});
A unit test should test one thing. Confine your it
blocks to a single assertion.
- Single responsibility principle
- A test can fail for only one reason
describe('Array.prototype', function() {
describe('.push(x)', function() {
it('appends x to the end of the Array and returns it', function() {
var initialArray = [1];
expect(initialArray.push(2)).toBe(2);
expect(initialArray).toEqual([1, 2]);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
it('appends x to the end of the Array', function() {
var initialArray = [1];
initialArray.push(2);
expect(initialArray).toEqual([1, 2]);
});
it('returns x', function() {
var initialArray = [1];
expect(initialArray.push(2)).toBe(2);
});
});
});
Organize your code in a way that clearly conveys the 3 A's of each unit test. One way to accomplish this is by Arranging and Acting in before
blocks and Asserting in it
ones.
- The AAA unit test pattern is well known and recommended
- Improves unit test modularity and creates opportunities to DRY things up
describe('Array.prototype', function() {
describe('.push(x)', function() {
it('appends x to the end of the Array', function() {
var initialArray = [1];
initialArray.push(2);
expect(initialArray).toEqual([1, 2]);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
var initialArray;
beforeEach(function() {
initialArray = [1]; // Arrange
initialArray.push(2); // Act
});
it('appends x to the end of the Array', function() {
expect(initialArray).toEqual([1, 2]); // Assert
});
});
});
Use before
/after
blocks to DRY up repeated setup, teardown, and action code.
- Keeps test suite more concise and readable
- Changes only need to be made in one place
- Unit tests are not exempt from coding best practices
describe('Array.prototype', function() {
describe('.push(x)', function() {
it('appends x to the end of the Array', function() {
var initialArray = [1];
initialArray.push(2);
expect(initialArray).toEqual([1, 2]);
});
it('returns x', function() {
var initialArray = [1];
expect(initialArray.push(2)).toBe(2);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
var initialArray,
pushResult;
beforeEach(function() {
initialArray = [1];
pushResult = initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(initialArray).toEqual([1, 2]);
});
it('returns x', function() {
expect(pushResult).toBe(2);
});
});
});
Use this
to share variables between it
and before
/after
blocks.
- Declare and initialize variables on one line
- Jasmine automatically cleans the
this
object between specs to avoid state leak
describe('Array.prototype', function() {
describe('.push(x)', function() {
var initialArray,
pushResult;
beforeEach(function() {
initialArray = [1];
pushResult = initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(initialArray).toEqual([1, 2]);
});
it('returns x', function() {
expect(pushResult).toBe(2);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
beforeEach(function() {
this.initialArray = [1];
this.pushResult = this.initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(this.initialArray).toEqual([1, 2]);
});
it('returns x', function() {
expect(this.pushResult).toBe(2);
});
});
});
Prefer beforeEach/afterEach
blocks over beforeAll/afterAll
ones. The latter are not reset between tests.
- Avoids accidental state leak
- Enforces test independence
- Order of
All
block execution relative toEach
ones is not always obvious
describe('Array.prototype', function() {
describe('.push(x)', function() {
beforeAll(function() {
this.initialArray = [1];
});
beforeEach(function() {
this.pushResult = this.initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(this.initialArray).toEqual([1, 2]);
});
it('returns x', function() {
expect(this.pushResult).toBe(2);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
beforeEach(function() {
this.initialArray = [1];
this.pushResult = this.initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(this.initialArray).toEqual([1, 2]);
});
it('returns x', function() {
expect(this.pushResult).toBe(2);
});
});
});
Nest describe
blocks liberally to create functional subsets.
- Allows tests to build on each other from least to most specific
- Creates tests that are easy to extend and/or refactor
- Makes branch testing easier and less repetitive
- Encapsulates tests based on their common denominator
describe('Array.prototype', function() {
describe('.push(x) on an empty Array', function() {
beforeEach(function() {
this.initialArray = [];
this.initialArray.push(1);
});
it('appends x to the Array', function() {
expect(this.initialArray).toEqual([1]);
});
});
describe('.push(x) on a non-empty Array', function() {
beforeEach(function() {
this.initialArray = [1];
this.initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(this.initialArray).toEqual([1, 2]);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
describe('on an empty Array', function() {
beforeEach(function() {
this.initialArray = [];
this.initialArray.push(1);
});
it('appends x to the Array', function() {
expect(this.initialArray).toEqual([1]);
});
});
describe('on a non-empty Array', function() {
beforeEach(function() {
this.initialArray = [1];
this.initialArray.push(2);
});
it('appends x to the end of the Array', function() {
expect(this.initialArray).toEqual([1, 2]);
});
});
});
});
If appropriate, use Jasmine's built-in matchers (such as toContain
, jasmine.any
, jasmine.stringMatching
, ...etc) to compare arguments and results. You can also create your own matcher via the asymmetricMatch
function.
- Tests become more resilient to future changes in the codebase
- Closer to testing behavior over implementation
describe('Array.prototype', function() {
describe('.push(x)', function() {
beforeEach(function() {
this.initialArray = [];
this.initialArray.push(1);
});
it('appends x to the Array', function() {
expect(this.initialArray).toEqual([1]);
});
});
});
describe('Array.prototype', function() {
describe('.push(x)', function() {
beforeEach(function() {
this.initialArray = [];
this.initialArray.push(1);
});
it('appends x to the Array', function() {
expect(this.initialArray).toContain(1);
});
});
});