What Are Sinon and Chai?
Testing JavaScript code effectively requires more than just assertion libraries. While Chai provides readable expectations, real-world applications often depend on external APIs, database calls, and third-party services that make testing unpredictable and slow. Sinon bridges this gap by providing mocks, stubs, and spies that isolate your code from dependencies.
Sinon.js is a standalone testing library that provides three essential utilities for comprehensive test coverage. Spies track function calls and their arguments without modifying behavior, allowing you to verify that specific functions are invoked correctly. Stubs extend this capability by replacing functions entirely with predefined behavior, giving you complete control over return values, error throwing, and asynchronous responses. Mocks combine stubs with expectations, creating test doubles that both replace behavior and verify interactions in a single step.
Chai complements Sinon by providing assertion syntax that reads like natural language. The library offers three assertion styles to match different coding preferences. The expect style follows BDD principles with chainable assertions that read fluently. The assert style provides straightforward function-based assertions for simpler use cases. The should style extends Object.prototype for those who prefer that syntax approach.
To get started with Sinon and Chai, install the packages along with your preferred test runner:
npm install mocha chai sinon --save-dev
For complete setup guidance, refer to the j-labs testing tutorial that covers installation and configuration patterns for Node.js projects. The LogRocket guide provides additional context on integrating these tools with Mocha as your test runner.
Incorporating robust testing practices is essential for any web development project, ensuring code quality and reducing production bugs.
Spies
Track function calls and arguments without changing behavior. Perfect for verifying that specific functions are invoked correctly.
Stubs
Replace functions with predefined behavior. Control return values, throw errors, or simulate async operations.
Mocks
Create mock objects with built-in expectations. Test complete object interactions with verification built-in.
Fake Timers
Control time for testing timeouts, delays, and scheduling without waiting in real-time.
Fake XMLHttpRequest
Mock HTTP requests in the browser for testing API interactions without network calls.
Fake Server
Combine fake timers and XHR for complete HTTP mocking in browser environments.
Spies: Observing Function Behavior
Spies are the most basic Sinon utility, wrapping functions to track calls without changing behavior. They're ideal for verifying that specific functions are invoked with expected arguments.
Creating Spies
// Create a spy for an existing function
const spy = sinon.spy(object, 'method');
// Create an anonymous spy
const spy = sinon.spy();
// Spy on a function
const wrappedSpy = sinon.spy(myFunction);
Tracking Call Information
Sinon provides comprehensive methods for inspecting how wrapped functions were called. The calledOnce and callCount properties reveal invocation frequency, while calledWith() verifies specific argument combinations. The args property exposes all arguments passed across every call as an array of arrays, and thisValues tracks the this context for each invocation.
For callback-heavy code, spies excel at verifying that handlers receive the correct arguments. Testing event handlers becomes straightforward when you can confirm exactly what arguments were passed during emission. The Sinon.js documentation provides complete coverage of spy API capabilities for both synchronous and asynchronous scenarios.
When combined with AI-assisted coding tools, developers can write more reliable tests faster while maintaining comprehensive coverage.
1// Create a spy2const mySpy = sinon.spy();3 4// Wrap a function5const wrappedFn = sinon.spy(myFunction);6 7// Call the function multiple times8wrappedFn('arg1');9wrappedFn('arg2');10 11// Verify calls12console.log(wrappedFn.calledOnce); // true13console.log(wrappedFn.callCount); // 214console.log(wrappedFn.args[0]); // ['arg1']15console.log(wrappedFn.calledWith('arg1')); // true16 17// Restore when done18wrappedFn.restore();Stubs: Replacing Dependencies with Controlled Behavior
Stubs extend spies by replacing functions entirely, allowing you to control return values, throw errors, or simulate async behavior. This is essential for testing code that depends on external services.
Creating Stubs
Stub creation follows similar patterns to spies but adds behavior configuration. Use stub.returns(value) for synchronous responses, stub.resolves(value) for successful promises, and stub.rejects(error) for failed promises. The onCall() method chains multiple behaviors for consecutive calls, enabling testing of retry logic or pagination scenarios.
For module-level stubbing, you can replace entire require statements with stubbed implementations. This pattern proves invaluable when testing functions that import dependencies, as you can simulate various scenarios without network calls or database connections. Remember to restore stubs after each test to prevent state leakage between tests.
The LogRocket testing guide demonstrates API testing patterns where stubs replace HTTP library calls with controlled responses for reliable, fast test execution.
1// Stub with return value2sinon.stub(database, 'query').returns([{ id: 1 }]);3 4// Stub with promise5sinon.stub(api, 'fetchUser')6 .resolves({ id: 1, name: 'John' });7 8// Stub with error9sinon.stub(api, 'fetchUser')10 .rejects(new Error('Not found'));11 12// Stub with different behavior per call13sinon.stub(api, 'fetchPage')14 .onFirstCall().resolves({ page: 1 })15 .onSecondCall().resolves({ page: 2 });16 17// After tests, restore18afterEach(() => {19 database.query.restore();20 api.fetchUser.restore();21});Chai Assertion Styles
Chai provides three assertion styles, each with different syntax and use cases. Choose based on team preferences and code readability.
Expect Style (BDD)
const expect = require('chai').expect;
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(beverage).to.have.property('flavour');
expect(users).to.have.lengthOf(3);
Assert Style
const assert = require('chai').assert;
assert.equal(foo, 'bar');
assert.typeOf(foo, 'string');
assert.property(users, 'id');
assert.lengthOf(users, 3);
Should Style
const should = require('chai').should;
foo.should.be.a('string');
foo.should.equal('bar');
beverage.should.have.property('flavour');
users.should.have.lengthOf(3);
The expect style is most widely adopted for its readability and chainability, following BDD principles that make tests read like specifications. Use assert style when you prefer function-based assertions without chaining, particularly useful for simpler test cases. The should style extends Object.prototype, which can cause issues with certain edge cases like testing null or undefined values, so use it judiciously.
The Chai.js documentation covers all three styles in detail, helping teams choose the approach that best fits their coding conventions. For guidance on structuring tests with these assertion styles, the SitePoint guide provides practical examples for both browser and Node.js testing.
Practical Integration Patterns
Testing API-Dependent Functions
Functions that call external APIs are notoriously difficult to test. Sinon stubs allow you to replace fetch calls or HTTP libraries with controlled responses. This pattern enables testing success paths, error handling, and edge cases without network dependencies.
When testing API-dependent code, create stubs for your HTTP client before each test, configure appropriate responses, and verify that your function calls the API correctly. Test both successful responses and failure scenarios to ensure your error handling works as expected. Verify that your function makes the correct API endpoint calls with proper parameters using spy assertions.
For comprehensive API testing patterns, the j-labs tutorial demonstrates how to stub external APIs effectively while maintaining test reliability. Combined with Chai's assertion syntax, you can write clear, maintainable tests that verify your API integration logic thoroughly.
Integrating testing practices into your AI code review workflows creates a powerful quality assurance system that catches issues before they reach production.
1// Function under test2async function getUserProfile(userId) {3 const response = await fetch(`/api/users/${userId}`);4 if (!response.ok) throw new Error('User not found');5 return response.json();6}7 8// Test with stubbed fetch9describe('getUserProfile', function() {10 beforeEach(function() {11 this.fetchStub = sinon.stub(global, 'fetch');12 });13 14 afterEach(function() {15 this.fetchStub.restore();16 });17 18 it('should return user data on success', async function() {19 const mockUser = { id: 1, name: 'Alice' };20 const mockResponse = {21 ok: true,22 json: sinon.stub().resolves(mockUser)23 };24 25 this.fetchStub.resolves(mockResponse);26 27 const result = await getUserProfile(1);28 29 expect(result).to.deep.equal(mockUser);30 expect(this.fetchStub.calledOnceWith('/api/users/1')).to.be.true;31 });32 33 it('should throw error on API failure', async function() {34 const mockResponse = { ok: false };35 this.fetchStub.resolves(mockResponse);36 37 await expect(getUserProfile(999))38 .to.be.rejectedWith('User not found');39 });40});Best Practices for Sinon and Chai
Writing Maintainable Tests
Test code requires the same care as production code. Well-structured tests are easier to read, debug, and maintain.
- Descriptive test names: Describe behavior, not implementation. Tests should read like specifications for your code's behavior.
- One assertion per test: When possible, keep tests focused on a single behavior to simplify debugging and understanding failures.
- Common setup: Use beforeEach hooks for repeated test setup to avoid duplication and ensure consistent test initialization.
- DRY in tests: Create helper functions for repeated setup patterns to keep tests clean while avoiding repetition.
Avoiding Common Pitfalls
Even experienced developers encounter issues with testing utilities. Understanding common mistakes prevents flaky tests and debugging frustration.
- Forgetting to restore stubs: Always call .restore() on stubs after each test. Use afterEach hooks or test framework cleanup mechanisms to ensure no state leaks between tests. Unrestored stubs can cause unpredictable behavior in subsequent tests.
- Over-mocking: Testing implementation details rather than behavior reduces test value. Focus on what your function does, not how it does it. When mocks obscure the actual behavior being tested, the test loses its protective value.
- Stubbing too much: Replacing too many dependencies makes tests pass but verify nothing meaningful. Stub only external dependencies and focus tests on your code's logic.
- Timing issues with async tests: Always handle promise resolution and rejection properly. Use async/await with proper try/catch, or ensure promise chains complete before assertions. Sinon provides .resolves() and .rejects() specifically for async testing scenarios.
For additional guidance on test structure and avoiding these pitfalls, refer to the SitePoint testing best practices guide.
Frequently Asked Questions
Conclusion
Sinon and Chai together provide a comprehensive testing toolkit for JavaScript applications. Spies reveal what's happening in your code, stubs control dependencies for reliable testing, and Chai's readable assertions verify outcomes. Combined with proper test structure and maintenance, this trio accelerates development while maintaining code quality.
Start with simple tests that verify basic functionality, then expand coverage strategically as your codebase grows. Invest time in creating maintainable test infrastructure with proper setup and teardown patterns. The investment pays dividends through faster debugging, confident refactoring, and higher code quality over time. For organizations looking to strengthen their development practices, our AI and automation services can help implement comprehensive testing strategies tailored to your technology stack.