YesNo HTTP Testing for Node.js

raw JSON →
0.0.7 verified Thu Apr 23 auth: no javascript

YesNo is an HTTP testing library for Node.js designed to intercept, mock, and record outgoing HTTP requests. Utilizing the `Mitm` library, it operates at a low level within the Node.js process, allowing for the inspection and manipulation of actual HTTP traffic generated by an application, rather than simply mocking higher-level request library calls. This approach enables more accurate and robust testing of an application's real-world network behavior. The current version, 0.0.7, is explicitly noted as a beta release, indicating that its API is subject to change and may undergo breaking modifications before reaching its first stable major release. The project is actively maintained, focusing on reaching this 1.0.0 milestone. Key features include spying on requests, easily mocking responses, and recording interactions for future use or to generate test data.

error Error: Mitm is already enabled!
cause The `yesno.spy()` method was called multiple times without a corresponding `yesno.restore()` call in between, leading to `Mitm` attempting to re-enable interception while already active.
fix
Ensure yesno.restore() is called after every test or test suite where yesno.spy() is used, typically within an afterEach or afterAll hook in your test runner.
error TypeError: Cannot read properties of undefined (reading 'response') or requests not being intercepted.
cause The `yesno.spy()` method was not active when the HTTP request was made, or `yesno.restore()` was called too early, before the asynchronous request completed.
fix
Verify that yesno.spy() is called *before* the code under test makes its HTTP requests, and yesno.restore() is called *after* all asynchronous operations related to the test are finished, often by ensuring the test case is async/await and awaits all network operations. Place yesno.spy() in beforeEach and yesno.restore() in afterEach for most unit test setups.
breaking YesNo is currently in beta (v0.0.7), and its API is explicitly noted as subject to change. Developers should expect potential breaking changes in minor or even patch versions before the first major stable release (1.0.0).
fix Consult the latest GitHub README and changelog for any API updates before upgrading. Pin exact versions of `yesno-http` in `package.json` to prevent unexpected breaks.
gotcha YesNo uses `Mitm` to intercept *all* outgoing network TCP and HTTP connections from the Node.js process. This can include unexpected traffic from other libraries or even Node.js internals, potentially interfering with other network-related test setups or debugging.
fix Use `yesno.restore()` diligently in `afterEach` hooks to ensure cleanup. For long-running connections or specific ports that should be ignored (e.g., database connections), configure `ignorePorts` in `yesno.spy(options: IInterceptOptions)`.
gotcha When intercepting HTTPS traffic, `Mitm` (and thus YesNo) might encounter issues with self-signed certificates or custom Certificate Authorities, leading to `ERR_CERT_AUTHORITY_INVALID` or similar trust errors.
fix Ensure your test environment is configured to trust the `Mitm` generated certificates if real HTTPS connections are being intercepted and re-encrypted. This often involves setting `NODE_TLS_REJECT_UNAUTHORIZED='0'` in test environments, but use with caution due to security implications, or specifically configuring certificate trust for your testing framework.
npm install yesno-http
yarn add yesno-http
pnpm add yesno-http

This quickstart demonstrates how to set up basic HTTP interception, mock responses, and retrieve specific intercepted requests using `yesno-http` within a test suite. It includes `beforeEach` and `afterEach` hooks for proper setup and teardown.

const { yesno } = require('yesno-http');
const { expect } = require('chai'); // Assuming chai for assertions
const http = require('http'); // For a simple HTTP request example

// Simulate a simple API call function
const myApi = {
  getUsers: async () => {
    return new Promise((resolve, reject) => {
      http.get('http://api.example.com/users', (res) => {
        let data = '';
        res.on('data', (chunk) => { data += chunk; });
        res.on('end', () => {
          try {
            resolve(JSON.parse(data));
          } catch (e) {
            reject(e);
          }
        });
      }).on('error', (err) => {
        reject(err);
      });
    });
  }
};

describe('my-api with YesNo', () => {
  beforeEach(() => {
    yesno.spy(); // Intercept requests before each test
  });

  afterEach(() => {
    yesno.restore(); // Clean up after each test to prevent interference
  });

  it('should get users and intercept the request', async () => {
    // Configure a mock response for 'api.example.com/users'
    yesno.mock([
      {
        url: 'http://api.example.com/users',
        response: {
          statusCode: 200,
          headers: { 'Content-Type': 'application/json' },
          body: { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] }
        }
      }
    ]);

    const users = await myApi.getUsers();
    const interceptedRequests = yesno.intercepted(); // Get the intercepted requests

    expect(interceptedRequests).to.have.lengthOf(1);
    expect(interceptedRequests[0]).to.have.nested.property('url', 'http://api.example.com/users');
    expect(users).to.deep.eql([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
    expect(interceptedRequests[0].response.statusCode).to.equal(200);
  });

  it('should allow filtering intercepted requests', async () => {
    yesno.mock([
      { url: 'http://api.example.com/posts', response: { statusCode: 200, body: [] } },
      { url: 'http://api.example.com/comments', response: { statusCode: 200, body: [] } }
    ]);

    await Promise.all([
      new Promise((resolve) => http.get('http://api.example.com/posts', resolve)),
      new Promise((resolve) => http.get('http://api.example.com/comments', resolve))
    ]);

    const postsRequests = yesno.intercepted(/posts/);
    expect(postsRequests).to.have.lengthOf(1);
    expect(postsRequests[0].url).to.include('/posts');
  });
});