How to test Node.js apps with Chai

Writing clear, readable test assertions is crucial for maintainable test suites that accurately verify application behavior. With over 12 years of Node.js development experience since 2014 and as the creator of CoreUI, I’ve written thousands of test assertions for production APIs. Chai is an assertion library that provides multiple assertion styles (expect, should, assert) with chainable, natural language syntax. This approach creates tests that read like documentation and clearly express what is being tested.

Use Chai assertion library to write expressive, readable test assertions with multiple assertion styles.

Install Chai:

npm install --save-dev chai chai-as-promised

Expect style (recommended):

const { expect } = require('chai')

describe('Expect assertions', () => {
  it('checks basic types', () => {
    expect(5).to.equal(5)
    expect('hello').to.be.a('string')
    expect(true).to.be.true
    expect(false).to.be.false
    expect(null).to.be.null
    expect(undefined).to.be.undefined
    expect([1, 2, 3]).to.be.an('array')
    expect({ name: 'John' }).to.be.an('object')
  })

  it('checks truthiness', () => {
    expect('hello').to.exist
    expect(null).to.not.exist
    expect(1).to.be.ok
    expect(0).to.not.be.ok
  })

  it('checks numbers', () => {
    expect(10).to.equal(10)
    expect(10).to.be.above(5)
    expect(10).to.be.below(15)
    expect(10).to.be.at.least(10)
    expect(10).to.be.at.most(10)
    expect(10).to.be.within(5, 15)
    expect(0.1 + 0.2).to.be.closeTo(0.3, 0.0001)
  })

  it('checks strings', () => {
    expect('hello world').to.include('world')
    expect('hello world').to.match(/^hello/)
    expect('hello').to.have.lengthOf(5)
    expect('hello').to.have.length.above(3)
  })

  it('checks arrays', () => {
    const arr = [1, 2, 3, 4]
    expect(arr).to.include(2)
    expect(arr).to.have.lengthOf(4)
    expect(arr).to.have.length.above(3)
    expect(arr).to.deep.equal([1, 2, 3, 4])
    expect(arr).to.include.members([1, 3])
    expect(arr).to.have.ordered.members([1, 2, 3, 4])
  })

  it('checks objects', () => {
    const obj = { name: 'John', age: 30, email: '[email protected]' }

    expect(obj).to.have.property('name')
    expect(obj).to.have.property('name', 'John')
    expect(obj).to.have.all.keys('name', 'age', 'email')
    expect(obj).to.include({ name: 'John' })
    expect(obj).to.deep.equal({ name: 'John', age: 30, email: '[email protected]' })
    expect(obj).to.have.own.property('age')
  })

  it('checks nested objects', () => {
    const obj = {
      user: {
        name: 'John',
        address: {
          city: 'New York'
        }
      }
    }

    expect(obj).to.have.nested.property('user.name', 'John')
    expect(obj).to.have.nested.property('user.address.city')
    expect(obj).to.have.deep.nested.property('user.address', { city: 'New York' })
  })
})

Testing async code with Chai:

const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)
const { expect } = chai

describe('Async assertions', () => {
  it('checks resolved promises', async () => {
    await expect(Promise.resolve('success')).to.eventually.equal('success')
    await expect(fetchData()).to.eventually.be.an('object')
    await expect(fetchData()).to.eventually.have.property('data')
  })

  it('checks rejected promises', async () => {
    await expect(Promise.reject(new Error('failed'))).to.be.rejected
    await expect(Promise.reject(new Error('failed'))).to.be.rejectedWith('failed')
    await expect(Promise.reject(new Error('failed'))).to.be.rejectedWith(Error)
  })

  it('checks promise fulfillment', async () => {
    await expect(Promise.resolve('value')).to.be.fulfilled
    await expect(Promise.resolve('value')).to.eventually.equal('value')
  })
})

Should style:

const chai = require('chai')
chai.should()

describe('Should assertions', () => {
  it('uses should syntax', () => {
    const name = 'John'
    name.should.be.a('string')
    name.should.equal('John')
    name.should.have.lengthOf(4)

    const arr = [1, 2, 3]
    arr.should.be.an('array')
    arr.should.include(2)
    arr.should.have.lengthOf(3)

    const obj = { name: 'John' }
    obj.should.be.an('object')
    obj.should.have.property('name')
  })
})

Assert style:

const { assert } = require('chai')

describe('Assert style', () => {
  it('uses assert syntax', () => {
    assert.equal(5, 5)
    assert.strictEqual(5, 5)
    assert.isTrue(true)
    assert.isFalse(false)
    assert.isNull(null)
    assert.isUndefined(undefined)
    assert.isString('hello')
    assert.isNumber(5)
    assert.isArray([1, 2, 3])
    assert.isObject({})
    assert.lengthOf([1, 2, 3], 3)
    assert.include([1, 2, 3], 2)
    assert.property({ name: 'John' }, 'name')
    assert.deepEqual({ a: 1 }, { a: 1 })
  })
})

Custom error messages:

describe('Custom messages', () => {
  it('adds context to failures', () => {
    const user = { name: 'John' }
    expect(user.age, 'User should have age property').to.exist
    expect(user.name, 'Name should be Jane').to.equal('Jane')
  })
})

Chai plugins:

const chai = require('chai')
const chaiHttp = require('chai-http')
chai.use(chaiHttp)

describe('HTTP assertions', () => {
  it('tests HTTP responses', async () => {
    const res = await chai.request(app).get('/api/users')

    expect(res).to.have.status(200)
    expect(res).to.be.json
    expect(res.body).to.be.an('array')
  })
})

Common assertion patterns:

describe('Patterns', () => {
  it('checks function throws', () => {
    const fn = () => { throw new Error('error') }
    expect(fn).to.throw()
    expect(fn).to.throw(Error)
    expect(fn).to.throw('error')
    expect(fn).to.throw(/error/)
  })

  it('checks function does not throw', () => {
    const fn = () => 'success'
    expect(fn).to.not.throw()
  })

  it('chains assertions', () => {
    expect([1, 2, 3])
      .to.be.an('array')
      .that.includes(2)
      .and.has.lengthOf(3)
  })

  it('uses negation', () => {
    expect(5).to.not.equal(6)
    expect('hello').to.not.include('world')
    expect([1, 2]).to.not.include(3)
  })
})

Best Practice Note

Use expect style for most tests—it’s more flexible than should and reads naturally. Use deep.equal to compare objects and arrays by value, not reference. Chai’s chainable syntax makes tests read like English. Use chai-as-promised for cleaner async assertions. Add custom error messages to provide context when tests fail. Use negation (.not) to test absence of properties or values. This is how we write assertions in CoreUI Node.js tests—clear, expressive Chai assertions that make test failures immediately understandable and test intent obvious to all developers.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to Merge Objects in JavaScript
How to Merge Objects in JavaScript

How to force a React component to re-render
How to force a React component to re-render

How to check if a string is a number in JavaScript
How to check if a string is a number in JavaScript

How to Get Unique Values from a JavaScript Array
How to Get Unique Values from a JavaScript Array

Answers by CoreUI Core Team