o-testing-toolbox
v2.0.1
Published
Additional expectations for `chai` package and other testing utilities
Downloads
8
Maintainers
Readme
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
andnycConfigFile
with your ownmocha
andnyc
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