o-testing-toolbox

2.0.1 • Public • Published

testing-toolbox

Additional expectations for chai package and other utilities

Installation

Install the package with

npm install --save-dev o-testing-toolbox

Usage

Defining variables with letBe statements

For this extension to work first you need to install the following packages:

npm install --save-dev mocha
npm install --save-dev chai

First include the LetBeExtesion plugin for chai in your test file

const chai = require('chai')
const {expect} = require('chai')
const {LetBeExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)

Then define a variable with

letBe.loginUrl = () => '/login'

(read it let loginUrl be '/login' like in math: let O be a not empty set)

If the value is a literal it is possible to use the shortcut

letBe.loginUrl = '/login'

Note that using the shortcut does not assign the value. It wraps it with an initializer. It also freezes the constant to avoid errors caused by secondary effects on the variable object. As a rule of thumb always use the first form.

letBe definitions can be anywhere in the test:

  • in the outer scope
beforeEach( () => {
  letBe.loginUrl = () => '/login'
})
 
describe('The login endpoint', () => {
  ...
})
  • in the inner scope
describe('The login endpoint', () => {
  beforeEach( () => {
    letBe.loginUrl = () => '/login'
  })
 
  ...
})
  • in the inner nested scope
describe('The login endpoint', () => {
  describe('returns 200', () => {
    beforeEach( () => {
      letBe.loginUrl = () => '/login'
    })
  })
 
  ...
})
  • in the test scope
describe('The login endpoint', () => {
  it('returns 200', () => {
    letBe.loginUrl = () => '/login'
  })
 
  ...
})

Avoid defining letBe statements out of a beforeEach or before block

// Not good. This assignment is global and it's executed only once when the file is required
letBe.loginUrl = () => '/login'
 
describe('The login endpoint', () => {
  it('returns 200', () => {
  })
 
  ...
})
// Good. This assigment is loaded once when the file is required but it's evaluated before each test
beforeEach( () => {
  letBe.loginUrl = () => '/login'
})
 
describe('The login endpoint', () => {
  it('returns 200', () => {
  })
 
  ...
})

To use a variable defined with letBe reference it like any other variable

before( () => {
  letBe.loginUrl = () => '/login'
})
 
describe('The login endpoint', () => {
  it('returns 200', () => {
    const response = httpClient.get(loginUrl)
 
    expect(response.statusCode).to.eql(200)
  })
})

It is possible to override letBe variables at any scope and still reference them from outer scopes

before( () => {
  letBe.httpClient = () => new HttpClient()
  letBe.userUrl = () => `/users/${userId}`
  letBe.httpResponse = () => httpClient.get(userUrl)
  letBe.responseStatusCode = () => httpResponse.statusCode
})
 
describe('The users endpoint', () => {
  describe('for an existent user', () => {
    beforeEach( () => {
      letBe.userId = () => 1
    })
 
    it('returns 200', () => {
      expect(responseStatusCode).to.eql(200)
    })
  })
 
  describe('for an inexistent user', () => {
    beforeEach( () => {
      letBe.userId = () => 2
    })
 
    it('returns 404', () => {
      expect(responseStatusCode).to.eql(404)
    })
  })
})

The differences between using a regular variable assigment

const loginUrl = '/login'
 
describe('The login endpoint', () => {
  ...
})

and a letBe definition are

  • a letBe variable is lazily initialized on its first use, not on its definition
  • (that means that) a letBe definition can reference other letBe definitions without caring about the order of declarations. That may or may not improve the understanding of the test but can be used to override letBe variables within inner scopes. See the previous example
  • also, it means that if a letBe variable is not used in a test it won't be initialized in that tests. Keep it in mind if your variable has expected side effects like creating items in a database
  • letBe variables can not be assigned with a value after its initialization. They behave like const variables
  • all letBe variables are always unset before and after each test run. That makes each test to start fresh and avoids side effects from previous tests

Defining helper methods with letBeMethod statements

If there is common or complex code in the tests is can be extracted to a method with the statement

letBeMethod.sum = function(a, b) { return a + b }

letBeMethod statements behave exactly like letBe variables.

Async expectation

Express expectations on Promises with

const chai = require('chai')
const {expect} = require('chai')
const {LetBeExtension, AsyncExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(AsyncExtension)
 
 
describe('A test expecting a promised value', () => {
  before(() => {
    letBe.promisedValue = () => Promise.resolve(1)
  })
 
  it('validates the promise resolution', (done) => {
    expect(promisedValue).to.eventually.be.above(0)
      .endWith(done)
  })
})

The expectation must end with .endWith(done)

Collection expectations

These assertions are available in the CollectionExtension

const chai = require('chai')
const {expect} = require('chai')
const {LetBeExtension, CollectionExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(CollectionExtension)
 
describe('A test', () => {
  it('expects a single item', () => {
    expect(aCollection).onlyItem.to.be ...
  })
  it('expects at least 1 item', () => {
    expect(aCollection).firstItem.to.be ...
  })
  it('expects at least a second item', () => {
    expect(aCollection).secondItem.to.be ...
  })
  it('expects at least a third item', () => {
    expect(aCollection).thirdItem.to.be ...
  })
  it('expects at least a n items', () => {
    expect(aCollection).atIndex(n).to.be ...
  })
  it('expects at least 1 last item', () => {
    expect(aCollection).lastItem.to.be ...
  })
})

These expectations usually get along well with .satisfy, .suchThat and .expecting

it('expects a collection with exactly one item', () => {
  expect(users).onlyItem.to.be.suchThat((user) => {
    expect(user.name).to.equal('John')
    expect(user.lastName).to.equal('Doe')
  })
})

and .samePropertiesThan

it('expects a collection with exactly one item', () => {
  expect(users).onlyItem.to.have.samePropertiesThan({
    name: 'John',
    lastName: 'Doe'
  })
})

chain expectations

These assertions are available in the ChainExtension

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(ChainExtension)

suchThat expectation

To express custom expectations on a nested attribute do

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(ChainExtension)
 
it('expects a collection with exactly one item with a given name', () => {
  expect(users).onlyItem.suchThat((user) => {
    expect(user.name).to.equal('John')
  })
})

make expectation

To assert that an action has some side effect on other objects do

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(ChainExtension)
 
it('expects a block to have an effect', () => {
  let value = 0
  expect(()=>{
    value += 1
  }).to.make(()=> value).to.equal(1)
})

Note that make always takes a block as its parameter and not a value. The reason is that a parameter would be evaluated before calling the .make assertion and therefore before the evaluation of the expect block.

This expectation can be used to test state after some event like a click or receiving a request occurs.

after expectation

Changes the subject of the assertion chain setting it to the result of the evaluation of the given block

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(ChainExtension)
 
it('expects a block to have an effect', () => {
  expect(1).after((n)=>n*10).to.equal(10)
})

This expectation can be combined with .make to express assertions like

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
it('expects a button to invoke its onClicked handler', () => {
  const user = new UserEmulator()
  let clickedValue = false
  const button = new Button({ onClicked: ()=>{clickedValue = true} })
 
  expect(button)
    .after((btn)=> user.click(btn))
    .to.make(()=>clickedValue).to.be.true
  })
})

as a simple alternative to the use of mock assertions.

expecting expectation

Takes a block with the subject as its parameter to allow multiple assertions on the subject.

For example

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
it('expects a list to have two items', () => {
  expect(list).expecting((list) => {
    list.to.have.firstItem.equalTo(10)
    list.to.have.secondItem.equalTo(11)
  })
})

With a regular assertions chain it would not be possible to make further assertions on the list object after the .firstItem assertion since it would have change the assertion subject.

After the evaluation of the block it is possible to continue with assertions on the original subject.

For example

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, ChainExtension} = require('o-testing-toolbox')
 
it('expects a list to have two items', () => {
  expect(list).expecting((list) => {
    list.to.have.firstItem.equalTo(10)
    list.to.have.secondItem.equalTo(11)
  }).to.have.length(2)
})

equalTo, eqlTo and matching expectations

equalTo, eqlTo and matching expectations are synonyms of equal, eql and match.

They are meant to be used when they might make an assertion more readable, for example

expect(component).to.have.div.with.text.equalTo('A text')

Json expectations

Assert expectations on nested structures with

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, JsonExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(JsonExtension)
 
describe('A test', () => {
  beforeEach(() => {
    letBe.expectedObject = {
      order: {
        id: 1,
        products: [
          oranges: 1
        ]
      }
    }
  })
 
  it('expects all the nested properties to match exactly', () => {
    expect(response.body).to.have.samePropertiesThan(expectedObject)
  })
})

If you need only a partial match do

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, JsonExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(JsonExtension)
 
describe('A test', () => {
  beforeEach(() => {
    letBe.expectedObject = {
      order: {
        products: [
          oranges: 1
        ]
      }
    }
  })
 
  it('expects the actual object to have the expectedProperties but it is ok if it has others', () => {
    expect(response.body).to.have.allPropertiesIn(expectedProperties)
  })
})

If you need to assert that the actual object does not have other properties than the expected ones do

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, JsonExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(JsonExtension)
 
describe('A test', () => {
  beforeEach(() => {
    letBe.expectedObject = {
      order: {
        products: [
          oranges: 1
        ]
      }
    }
  })
 
  it('expects the actual object not to have other properties that the ones in expectedProperties', () => {
    expect(response.body).to.have.noOtherPropertiesThan(expectedProperties)
  })
})

If you need to assert on some nested property for a custom expectation use a function instead of a value:

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, JsonExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(JsonExtension)
 
describe('A test', () => {
  beforeEach(() => {
    letBe.expectedObject = {
      id: (value) => { expect(value).to.be.a('integer').above(0) }  // <-- custom expectation
      order: {
        products: [
          oranges: 1
        ]
      }
    }
  })
 
  it('expects the actual object not to have other properties that the ones in expectedProperties', () => {
    expect(response.body).to.have.allPropertiesIn(expectedProperties)
  })
})

This expectation block is useful to test for a text to match a regular expression and for a float to equal a constant

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, JsonExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(JsonExtension)
 
describe('A test', () => {
  beforeEach(() => {
    letBe.expectedObject = {
      description: (value) => { expect(description).to.match(/Product:/) }
      price: (value) => { expect(price).to.be.closeTo(1, 0.001) }
    }
  })
 
  it('expects an object property with a float and a text to satisfy custom assertions', () => {
    expect(response.body).to.have.allPropertiesIn(expectedProperties)
  })
})

File expectations

Assert expectations on a file with

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, FileExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(FileExtension)
 
describe('A test', () => {
  it('expects a file to exist', () => {
    expect('/path/to/someFile.txt').to.be.a.file
  })
 
  it('expects a file to have a content', () => {
    expect('/path/to/someFile.txt').fileContents.to.match(/expression/)
  })
 
  it('expects a directory to exist', () => {
    expect('/path/to/someDirectory').be.a.directory
  })
})

The expected path

expect(path)

can be a String or any other object that implements a .toString() method. Particularly it could be a Path object with a path.toString() method.

Disposable files helper

To create temporary files or directories during the tests use

const chai = require('chai')
const { expect } = require('chai')
const { TmpDir } = require('o-testing-toolbox')
 
beforeEach(()=>{
  const tmpDir = new TmpDir()
  const stylesDir = tmpDir.createDir('styles/')
  const scriptFile = tmpDir.createFile({ path: 'scripts/main.js', contents: 'const a = 1' })
})

The files and directories created through TmpDir are unique for each test execution and reside in the /tmp directory of your operative system and will be eventually discarded.

Skipping slow tests

For this extension to work first you need to install the following packages:

npm install --save-dev mocha
npm install --save-dev chai

Mocha reports tests that take longer to run than a configurable threshold.

In a regresion test you would like to run all tests but while you are developing a feature you may want to skip the slow ones.

To skip slow tests first run all the tests as usual to see which ones are reported to be slow

npm test

Once you have identified the slow ones on each slow test file include the SlowTestsExtension plugin for chai

const chai = require('chai')
const { expect } = require('chai')
const {SlowTestsExtension} = require('o-testing-toolbox')
 
chai.use(SlowTestsExtension)

and replace the original test

describe('...', () => {
  it('this test is slow', () => {
    // ...
  })
})

with

describe('...', () => {
  slow.it('this test is slow', () => {
    // ...
  })
})

It's also possible to flag an entire group as slow:

slow.describe('...', () => {
  it('this test is slow', () => {
    // ...
  })
 
  it('this one too', () => {
    // ...
  })
})

Finally if you want to run the tests skipping the ones flagged as slow do

skip_slow=true npm test

To run all the tests execute

npm test

as usual.

Expected code styles

For this extension to work first you need to install the following packages:

npm install --save-dev mocha
npm install --save-dev chai

npm install --save-dev eslint
npm install --save-dev eslint-config-standard
npm install --save-dev eslint-plugin-import
npm install --save-dev eslint-plugin-node
npm install --save-dev eslint-plugin-promise"
npm install --save-dev eslint-plugin-standard"

To test that your source code complies with the coding standards and best practices create the test

const path = require('path')
const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, SourceCodeExtension, SlowTestsExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(SourceCodeExtension)
chai.use(SlowTestsExtension)
 
 
describe('Coding style', () => {
  before(() => {
    letBe.eslintConfigFile = './utilities-config/.eslintrc.js'
  })
 
  slow.it('complies with standards', () => {
    expect().to.complyWithCodingStyles({
      eslintConfigFile: eslintConfigFile
    })
  })
})

Replace the variable letBe.eslintConfigFile with your own eslint config file or delete it if you don't use a custom config file.

Expected code coverage

For this extension to work first you need to install the following packages:

npm install --save-dev mocha@7
npm install --save-dev chai
 
npm install --save-dev nyc

Note that for this extension mocha version must be mocha@7 due to breaking changes in version mocha@8

To test that the tests coverage is above an expected minimum create a new test file with the following contents:

const chai = require('chai')
const { expect } = require('chai')
const {LetBeExtension, SourceCodeExtension, SlowTestsExtension} = require('o-testing-toolbox')
 
chai.use(LetBeExtension)
chai.use(SourceCodeExtension)
chai.use(SlowTestsExtension)
 
const linesCoverageTarget = 100
 
describe('Testing suite covers', () => {
    before( () => {
        letBe.coveredLinesPercentage = linesCoverageTarget
        letBe.thisFilename = path.basename(__filename)
        letBe.fileExclusionPattern = `'**/${thisFilename}',`
        letBe.mochaConfigFile = './utilities-config/.mocharc.json'
        letBe.nycConfigFile = './utilities-config/nyc.config.json'    
    })
 
      slow.it(`${linesCoverageTarget}% of the lines of code`, () => {
        expect().linesCoverage({
          excluding: fileExclusionPattern,
          mochaConfigFile: mochaConfigFile,
          nycConfigFile: nycConfigFile
        }).to.be.at.least(coveredLinesPercentage)
    })
})

Notes:

  • This test file is excluded for the coverage test otherwise it would create an infinite recursion.
  • Replace the variable letBe.mochaConfigFile and nycConfigFile with your own mocha and nyc config files or delete them if you don't use a custom config file.
  • The coverage test is always flagged as slow. You can skip running the tests with
skip_slow=true npm test

Package Sidebar

Install

npm i o-testing-toolbox

Weekly Downloads

1

Version

2.0.1

License

ISC

Unpacked Size

68.4 kB

Total Files

35

Last publish

Collaborators

  • haijindev