Jest is a popular JavaScript testing framework that provides an intuitive and powerful way to write automated tests for your codebase. One of the features that make Jest stand out is its test.each function, which enables you to write more concise and readable tests by parameterizing test cases.

With test.each, you can define an array of test cases and run the same test function against each test case with the input arguments substituted. This feature is particularly useful when you need to test a function with a large number of input combinations, making your test code more maintainable and less verbose.

In this blog post, you’ll see an example of how I refactored several repetitive tests to use test.each in Jest and simplify my test code.

Continue Reading ...

When validating a note, bookmark or snippet on Codever several validation rules might be broken and the response given to the consumer will contain all the corresponding error messages.

In the following example we will consider the case of notes and how we can verify in jest repetitive tests if a broken rule is present in the error response. To do that we surround the method under test in a try catch block and use jest expect.arrayContaining assertion on the validationErrors array in the error content

describe('validateNoteInput', () => {
  test.each([
    // Test cases go here as an array of arrays
    // Each inner array represents a set of arguments to pass to the function
    // The last element of the inner array is the expected error message
    [123, { userId: null, title: 'Test', content: 'This is a test note' }, NoteValidationErrorMessages.MISSING_USER_ID],
    [456, { userId: 789, title: 'Test', content: 'This is a test note' }, NoteValidationErrorMessages.USER_ID_NOT_MATCHING],
    [111, { userId: 111, title: null, content: 'This is a test note' }, NoteValidationErrorMessages.MISSING_TITLE],
    [222, { userId: 222, title: 'Test', content: null }, NoteValidationErrorMessages.MISSING_CONTENT],
    [333, { userId: 333, title: 'Test', content: 'x'.repeat(NoteValidationRules.MAX_NUMBER_OF_CHARS_FOR_CONTENT + 1) }, NoteValidationErrorMessages.CONTENT_TOO_LONG],
  ])('throws a ValidationError with the correct error message', (userId, note, expectedErrorMessage) => {
    try {
    expect(() => noteInputValidator.validateNoteInput(userId, note)).toThrowError(ValidationError);
    expect(() => noteInputValidator.validateNoteInput(userId, note)).toThrowError(NoteValidationErrorMessages.NOTE_NOT_VALID);
    } catch (error) {
      // If the function threw an error, test that the error message is correct
      expect(error.validationErrors).toEqual(expect.arrayContaining(expectedErrorMessage));
    }
  });
});

Project: codever - File: note-input.validator.test.js

The method under test is defined in the following snippet

const ValidationError = require('../../../error/validation.error');

let validateNoteInput = function (userId, note) {

  let validationErrorMessages = [];

  if ( !note.userId ) {
    validationErrorMessages.push(NoteValidationErrorMessages.MISSING_USER_ID);
  }

  if ( note.userId !== userId ) {
    validationErrorMessages.push(NoteValidationErrorMessages.MISSING_USER_ID);
  }

  if ( !note.title ) {
    validationErrorMessages.push(NoteValidationErrorMessages.MISSING_TITLE);
  }

  if ( !note.content ) {
    validationErrorMessages.push(NoteValidationErrorMessages.MISSING_CONTENT);
  }

  if ( note.content ) {
    const descriptionIsTooLong = note.content.length > NoteValidationRules.MAX_NUMBER_OF_CHARS_FOR_CONTENT;
    if ( descriptionIsTooLong ) {
      validationErrorMessages.push(NoteValidationErrorMessages.CONTENT_TOO_LONG);
    }
  }

  if ( validationErrorMessages.length > 0 ) {
    throw new ValidationError(NoteValidationErrorMessages.NOTE_NOT_VALID, validationErrorMessages);
  }
}

const NoteValidationRules = {
  MAX_NUMBER_OF_CHARS_FOR_CONTENT: 10000,
  MAX_NUMBER_OF_TAGS: 8
}

const NoteValidationErrorMessages = {
  NOTE_NOT_VALID: 'The note you submitted is not valid',
  MISSING_USER_ID: 'Missing required attribute - userId',
  USER_ID_NOT_MATCHING: 'The userId of the bookmark does not match the userId parameter',
  MISSING_TITLE: 'Missing required attribute - title',
  MISSING_CONTENT: 'Missing required attribute - content',
  CONTENT_TOO_LONG: `The content is too long. Only ${NoteValidationRules.MAX_NUMBER_OF_CHARS_FOR_CONTENT} allowed`,
}

module.exports = {
  validateNoteInput: validateNoteInput,
  NoteValidationRules: NoteValidationRules,
  NoteValidationErrorMessages: NoteValidationErrorMessages
};

Reference - https://github.com/CodeverDotDev/codever


Shared with from Codever. 👉 Use the Copy to mine functionality to copy this snippet to your own personal collection and easy manage your code snippets.

Codever is open source on Github ⭐🙏

For the following req object there is a need to override the stackoverflowId attribute in a jest test

const req = {
  body: {
    _id: '123',
    name: 'Codever',
    location: 'https://www.codever.dev',
    language: 'en',
    description: 'This is a test bookmark',
    tags: ['test'],
    public: true,
    stackoverflowQuestionId: null
  },
  params: {
    userId: '456',
  },
};

Project: codever - File: bookmark-request.mapper.test.js

Here is how to achieve that with the spread operator

      {...req,
        body : {
        ...req.body,
          stackoverflowQuestionId: 123456
        }
      }

The complete function where this is used is shown below:

 test.each([
    [
      'should set stackoverflowQuestionId if it is provided',
      {...req,
        body : {
        ...req.body,
          stackoverflowQuestionId: 123456
        }
      },
      new Bookmark({
        _id: '123',
        name: 'Codever',
        location: 'https://www.codever.dev',
        language: 'en',
        description: 'This is a test bookmark',
        descriptionHtml: '<p>This is a test bookmark</p>',
        tags: ['test'],
        public: true,
        userId: '456',
        likeCount: 0,
        youtubeVideoId: null,
        stackoverflowQuestionId: 123456,
      })
    ],
  ])('%s', (testname, req, expectedBookmark) => {
    const resultBookmark = bookmarkRequestMapper.toBookmark(req);

    expect({...resultBookmark.toObject(), _id: {}}).toEqual({...expectedBookmark.toObject(), _id: {}});
    expect(showdown.Converter).toHaveBeenCalledTimes(1);
    expect(showdown.Converter().makeHtml).toHaveBeenCalledTimes(1);
    expect(showdown.Converter().makeHtml).toHaveBeenCalledWith('This is a test bookmark');
  });

Reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax


Shared with from Codever. 👉 Use the Copy to mine functionality to copy this snippet to your own personal collection and easy manage your code snippets.

Codever is open source on Github ⭐🙏

One easy way to display a custom name for each test.each test in jest, is to place the text name in the first element of each array of the input table for the method and use it as name in the test.each method - ('%s', (testname, req, expectedBookmark) , as in the following snippet:

const showdown = require('showdown');
const Bookmark = require('../../model/bookmark');
const bookmarkRequestMapper = require('./bookmark-request.mapper');

jest.mock('showdown', () => {
  const makeHtml = jest.fn(() => '<p>This is a test bookmark</p>');
  return {
    Converter: jest.fn().mockImplementation(() => ({makeHtml})),
  };
});

describe('toBookmark', () => {
  const req = {
    body: {
      _id: '123',
      name: 'Test Bookmark',
      location: 'https://example.com',
      language: 'en',
      description: 'This is a test bookmark',
      tags: ['test'],
      public: true,
    },
    params: {
      userId: '456',
    },
  };

  beforeEach(() => {
    showdown.Converter.mockClear();
    showdown.Converter().makeHtml.mockClear();
    req.body.descriptionHtml = undefined;
    req.body.youtubeVideoId = undefined;
    req.body.stackoverflowQuestionId = undefined;
  });

  test.each([
    [
      'should return a new bookmark',
      req,
      new Bookmark({
        _id: 123,
        name: 'Test Bookmark',
        location: 'https://example.com',
        language: 'en',
        description: 'This is a test bookmark',
        descriptionHtml: '<p>This is a test bookmark</p>',
        tags: ['test'],
        public: true,
        userId: '456',
        likeCount: 0,
        youtubeVideoId: null,
        stackoverflowQuestionId: null,
      })
    ],
    [
      'should set youtubeVideoId if it is provided',
      {...req,
        body : {
        ...req.body,
          youtubeVideoId: 'abcd1234'
        }
      },
      new Bookmark({
        _id: '123',
        name: 'Test Bookmark',
        location: 'https://example.com',
        language: 'en',
        description: 'This is a test bookmark',
        descriptionHtml: '<p>This is a test bookmark</p>',
        tags: ['test'],
        public: true,
        userId: '456',
        likeCount: 0,
        youtubeVideoId: 'abcd1234',
        stackoverflowQuestionId: null,
      })
    ],
    [
      'should set stackoverflowQuestionId if it is provided',
      {...req,
        body : {
        ...req.body,
          stackoverflowQuestionId: 123456
        }
      },
      new Bookmark({
        _id: '123',
        name: 'Test Bookmark',
        location: 'https://example.com',
        language: 'en',
        description: 'This is a test bookmark',
        descriptionHtml: '<p>This is a test bookmark</p>',
        tags: ['test'],
        public: true,
        userId: '456',
        likeCount: 0,
        youtubeVideoId: null,
        stackoverflowQuestionId: 123456,
      })
    ],
  ])('%s', (testname, req, expectedBookmark) => {
    const resultBookmark = bookmarkRequestMapper.toBookmark(req);

    expect({...resultBookmark.toObject(), _id: {}}).toEqual({...expectedBookmark.toObject(), _id: {}});
    expect(showdown.Converter).toHaveBeenCalledTimes(1);
    expect(showdown.Converter().makeHtml).toHaveBeenCalledTimes(1);
    expect(showdown.Converter().makeHtml).toHaveBeenCalledWith('This is a test bookmark');
  });
});

Project: codever - File: bookmark-request.mapper.test.js

Of course you are free to display other data where appropiate…

Reference - https://jestjs.io/docs/api#testeachtablename-fn-timeout


Shared with from Codever. 👉 Use the Copy to mine functionality to copy this snippet to your own personal collection and easy manage your code snippets.

Codever is open source on Github ⭐🙏

The schema model object in Mongoose provides an _id that is of type ObjectId. If you are not interested in comparing value of this attribute when you compare it compare objects in jest, you can exclude it by calling the toObject method of the mongoose model and set the _id object to nothing via the spread operator, something similar to the following:

expect({...resultBookmark.toObject(), _id: {}}).toEqual({...expectedBookmark.toObject(), _id: {}})

The complete testing method with the setup is shown in the snippet bellow, where expected bookmark model should match the mapped bookmark from the given request:

const showdown = require('showdown');
const Bookmark = require('../../model/bookmark');
const bookmarkRequestMapper = require('./bookmark-request.mapper');

jest.mock('showdown', () => {
  const makeHtml = jest.fn(() => '<p>This is a test bookmark</p>');
  return {
    Converter: jest.fn().mockImplementation(() => ({makeHtml})),
  };
});

describe('toBookmark', () => {
  const req = {
    body: {
      _id: '123',
      name: 'Test Bookmark',
      location: 'https://example.com',
      language: 'en',
      description: 'This is a test bookmark',
      tags: ['test'],
      public: true,
    },
    params: {
      userId: '456',
    },
  };

  beforeEach(() => {
    showdown.Converter.mockClear();
    showdown.Converter().makeHtml.mockClear();
    req.body.descriptionHtml = undefined;
    req.body.youtubeVideoId = undefined;
    req.body.stackoverflowQuestionId = undefined;
  });

  it('should use descriptionHtml if it is provided', () => {
    req.body.descriptionHtml = '<p>This is a test bookmark</p>';

    const expectedBookmark = new Bookmark({
      _id: '123',
      name: 'Test Bookmark',
      location: 'https://example.com',
      language: 'en',
      description: 'This is a test bookmark',
      descriptionHtml: '<p>This is a test bookmark</p>',
      tags: ['test'],
      public: true,
      userId: '456',
      likeCount: 0,
      youtubeVideoId: null,
      stackoverflowQuestionId: null,
    });

    const resultBookmark = bookmarkRequestMapper.toBookmark(req);

    expect({...resultBookmark.toObject(), _id: {}}).toEqual({...expectedBookmark.toObject(), _id: {}})
    expect(showdown.Converter().makeHtml).not.toHaveBeenCalled();
  });
});

Project: codever - File: bookmark-request.mapper.test.js


Shared with from Codever. 👉 Use the Copy to mine functionality to copy this snippet to your own personal collection and easy manage your code snippets.

Codever is open source on Github ⭐🙏