should review
should 13.2.3 is a test assertion library that adds fluent checks for equality, properties, types, numbers, strings, collections, thrown errors, and promises without choosing a test runner. Its default entry point installs a non-enumerable should getter on Object.prototype, producing chains such as user.should.have.property('id'). A separate should/as-function entry point keeps Object.prototype untouched and also works with null or Object.create(null) values. Our Node 22 install loaded through require() and ESM import, bundled TypeScript declarations, and brought 5 direct dependencies. The current version fixed .only.keys when the prototype getter is absent; it shipped in July 2018, and the GitHub repository is archived.
should 13.2.3 installed in 1.7 seconds as 6 packages using 1 MB with 0 audit findings in our sandbox, but its release is from 2018 and its repository is archived. Keep it for an existing fluent suite; choose maintained assertions for new work and use should/as-function wherever prototype changes are off limits.
We installed it
| Install | ✓ · 1.7s | 6 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 11.6 KB | gzipped (42.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does should install cleanly?
Yes. In a fresh container with an empty cache, npm install should finished in 2 seconds, leaving 6 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does should add to a browser bundle?
11.6 KB gzipped (42.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does should work with both ESM and CommonJS?
Yes. Both import 'should' and require('should') worked in Node 22 in our run. The package is published as CommonJS.
Does should include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
should or chai: which should you use?
chai: Use it for actively released BDD or TDD assertions and a current plugin ecosystem around common JavaScript test runners. should 13.2.3 installed in 1.7 seconds as 6 packages using 1 MB with 0 audit findings in our sandbox, but its release is from 2018 and its repository is archived.
When should you not use should?
You are choosing assertions for a new project. The repository is archived, 13.2.3 dates to July 2018, and the last GitHub push was in October 2019.
Use it if
- An established suite already uses should.js chains, custom assertions, or plugins, and rewriting failures would cost more than keeping the frozen dependency.
- Tests run under Mocha or another runner and only need an assertion layer rather than a runner, mocking system, and coverage stack.
- The team can standardize on should/as-function to avoid changing Object.prototype while retaining the existing assertion vocabulary.
- Legacy code depends on should.js checks for Map, Set, promises, deep containment, or custom Assertion.add extensions.
- You are choosing assertions for a new project. The repository is archived, 13.2.3 dates to July 2018, and the last GitHub push was in October 2019.
- Production policy rejects prototype modification. require('should') adds a getter to Object.prototype; every test entry must consistently use should/as-function to avoid it.
- The toolchain expects an ESM-first package with an exports map. should is CommonJS, has no exports map, and documents CommonJS-era loading even though our ESM import check worked.
- Current TypeScript behavior must be maintained against new compiler releases. The bundled should.d.ts came from a package whose development setup used TypeScript 2.5.3.
- Browser tests need a maintained module and compatibility path. The README directs users to a checked-in UMD file, a known-bugs wiki, and even Bower-era installation.
Setup reality
Our clean Node 22 sandbox installed should 13.2.3 in 1.7 seconds. Six packages occupied 1 MB, while the should package itself is 428 KB unpacked. npm audit found 0 known vulnerabilities at critical, high, moderate, and low severity. It declares 5 direct dependencies and 0 peers, includes TypeScript declarations, and uses the MIT license. CommonJS require() and ESM import both worked in our measurements.
No native build, credentials, peer setup, or config file is required. The first import decides whether the process is modified. require('should') defines a non-enumerable should getter on Object.prototype. require('should/as-function') avoids that change and supports null plus objects with no prototype. Mixing the two entry points across test setup files makes behavior depend on load order, so choose one convention for the entire suite.
Fluent filler properties such as be, have, which, and and perform no check. property() and length() are different: they move the active assertion to the selected value. Keep chains short enough that the current subject is clear. Promise assertions return promises, so the test must await or return fulfilled(), rejected(), resolvedWith(), or rejectedWith(). A runner can otherwise finish before a late rejection reaches the assertion.
The package is CommonJS without an exports map, despite successful ESM interop on our Node 22 box. Its bundled types and browser instructions reflect the 2018 toolchain. Our esbuild browser test produced 42.4 KB minified and 11.6 KB gzipped. That is avoidable client weight for test code, and the repository's browser route relies on an old UMD bundle. Keep it in devDependencies and out of production bundles.
Patterns
Assert through the function-only entry avoid-prototype-change
const should = require('should/as-function')
should(result).have.property('status', 200)
should(null).not.be.ok()should/as-function does not install the Object.prototype getter. It is also the usable form for null and Object.create(null) values.
Check and continue from a property assert-property
const should = require('should/as-function')
should(user)
.have.property('profile')
.which.have.property('name', 'Ada')property('profile') changes the active assertion subject to user.profile. The next property check runs against that nested value.
Compare nested values compare-deeply
should(actual).eql({
id: 7,
roles: ['editor'],
})eql() performs value-oriented deep comparison. equal() and exactly() use strict identity semantics.
Require an exact key set check-exact-keys
should(payload).only.have.keys('id', 'name')The only modifier makes keys() reject extra keys. Version 13.2.3 fixed this form when Object.prototype.should is absent.
Match part of a nested value check-deep-containment
should(response).containDeep({
user: { roles: ['admin'] },
})containDeep() checks nested partial content. Use eql() when extra fields or collection members should fail the assertion.
Check a measured tolerance assert-number-range
should(durationMs).be.within(95, 105)
should(ratio).be.approximately(0.5, 0.01)within() includes the supplied bounds. approximately() compares against the stated delta.
Match a synchronous exception assert-thrown-error
should(() => parseConfig('bad')).throw(/invalid config/)
should(() => parseConfig(null)).throw(TypeError)throw() invokes the function synchronously. Promise rejections need rejected() or rejectedWith() instead.
Await a rejected promise assert-rejected-promise
await should(loadUser(-1)).be.rejectedWith(/invalid id/)rejectedWith() returns a promise. Await or return it so the test runner observes the final assertion result.
Check a fulfilled promise value assert-resolved-value
await should(Promise.resolve({ id: 7 })).be.fulfilledWith({ id: 7 })fulfilledWith() performs the promised-value assertion asynchronously and returns a promise to the test.
Assert Set or Map size check-collection-size
should(new Set(['a', 'b'])).have.size(2)
should(new Map([['id', 7]])).have.key('id')The 13.2 line supports Set as an iterable through its type-adaptor dependency. size() works across supported collection types.
Define a project assertion add-custom-assertion
const should = require('should/as-function')
should.Assertion.add('userId', function () {
this.params = { operator: 'to be a user id' }
should(this.obj).match(/^usr_[0-9]+$/)
})
should('usr_42').be.a.userId()Assertion.add() expects the callback to set this.params before checking the positive case. should.js handles .not itself.
Remove the default global getter remove-global-getter
const should = require('should').noConflict()
should(Object.prototype).not.have.property('should')
should(value).be.ok()noConflict() removes the getter installed by the default entry and returns the should function. Prefer should/as-function when no global change is wanted at all.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| chai | npm | Use it for actively released BDD or TDD assertions and a current plugin ecosystem around common JavaScript test runners. |
| expect | npm | Use it when Jest-style expect(value) matchers fit the suite and prototype modification is undesirable. |
| power-assert | npm | Use it for richer output from ordinary assert expressions when an older transform-based workflow is already part of the project. |
More testing guides
pytest · chai · jsdom · vitest · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

