DEV Community

Adrian Matei for CodepediaOrg

Posted on • Originally published at codepedia.org

How to use test.each to avoid repetitive tests in jest

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 parameterising 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.

The method under test

The method under test is setSpecialSearchTermsFilter, which extends the document search filter with special ones you can set on Codever:

let setSpecialSearchTermsFilter = function (docType, isPublic, userId, specialSearchFilters, filter) {
  let newFilter = {...filter};

  //one is not entitled to see private bookmarks of another user
  if ( specialSearchFilters.userId && (isPublic || specialSearchFilters.userId === userId) ) {
    newFilter.userId = specialSearchFilters.userId;
  }

  if ( specialSearchFilters.privateOnly && !isPublic ) { //
    newFilter.public = false;
  }

  if ( specialSearchFilters.site ) {
    if(docType === DocType.BOOKMARK) {
      newFilter.location = new RegExp(specialSearchFilters.site, 'i');
    } else if (docType === DocType.SNIPPET) {
      newFilter.sourceUrl = new RegExp(specialSearchFilters.site, 'i');//TODO when performance becomes an issue extract domains from URLs and make a direct comparison with the domain
    } else {
      throw new Error(`${docType} is not supported as document type`)
    }
  }

  return newFilter;
};
Enter fullscreen mode Exit fullscreen mode

Before

In the snippet below you can see the tests before using jest test.each

describe('setSpecialSearchTermsFilter', () => {
  it('should set the userId filter when specialSearchTerms.userId is present and isPublic is true', () => {
    const filter = {};
    const specialSearchTerms = {userId: '123'};
    const result = searchUtils.setSpecialSearchTermsFilter(DocType.SNIPPET, true, '123', specialSearchTerms, filter);
    expect(result).toEqual({userId: '123'});
  });

  it('should set the userId filter when specialSearchTerms.userId is present and matches the userId', () => {
    const filter = {};
    const specialSearchTerms = {userId: '123'};
    const result = searchUtils.setSpecialSearchTermsFilter(DocType.SNIPPET, false, '123', specialSearchTerms, filter);
    expect(result).toEqual({userId: '123'});
  });

  it('should not set the userId filter when specialSearchTerms.userId is present and does not match the userId', () => {
    const filter = {};
    const specialSearchTerms = {userId: '456'};
    const result = searchUtils.setSpecialSearchTermsFilter(DocType.SNIPPET, false, '123', specialSearchTerms, filter);
    expect(result).toEqual({});
  });

  it('should set the public filter to false when specialSearchTerms.privateOnly is present', () => {
    const filter = {};
    const specialSearchTerms = {privateOnly: true};
    const result = searchUtils.setSpecialSearchTermsFilter(DocType.SNIPPET, false, '123', specialSearchTerms, filter);
    expect(result).toEqual({public: false});
  });

  it('should set the sourceUrl filter when specialSearchTerms.site is present for snippets', () => {
    const filter = {};
    const specialSearchTerms = {site: 'example.com'};
    const result = searchUtils.setSpecialSearchTermsFilter(DocType.SNIPPET, false, '123', specialSearchTerms, filter);
    expect(result).toEqual({sourceUrl: /example.com/i});
  });

  it('should set the location filter when specialSearchTerms.site is present for bookmarks', () => {
    const filter = {};
    const specialSearchTerms = {site: 'example.com'};
    const result = searchUtils.setSpecialSearchTermsFilter(DocType.BOOKMARK, false, '123', specialSearchTerms, filter);
    expect(result).toEqual({location: /example.com/i});
  });

  it('should set the location filter when specialSearchTerms.site is present for bookmarks', () => {
    const filter = {};
    const specialSearchTerms = {site: 'example.com'};
    expect(() => searchUtils.setSpecialSearchTermsFilter('unknown', false, '123', specialSearchTerms, filter)).toThrow(Error);
  });
});
Enter fullscreen mode Exit fullscreen mode

After

By using test.each the test become more concise and more rapid to read

describe('setSpecialSearchTermsFilter', () => {
  describe('valid calls', () => {
    test.each([
      [{}, DocType.SNIPPET, {userId: '123'}, true, '123', {userId: '123'}],
      [{}, DocType.SNIPPET, {userId: '123'}, false, '123', {userId: '123'}],
      [{}, DocType.SNIPPET, {userId: '456'}, false, '123', {}],
      [{}, DocType.SNIPPET, {privateOnly: true}, false, '123', {public: false}],
      [{}, DocType.SNIPPET, {site: 'example.com'}, false, '123', {sourceUrl: /example.com/i}],
      [{}, DocType.BOOKMARK, {site: 'example.com'}, false, '123', {location: /example.com/i}]
    ])('should set the filter correctly', (filter, docType, specialSearchTerms, isPublic, userId, expected) => {
      const result = searchUtils.setSpecialSearchTermsFilter(docType, isPublic, userId, specialSearchTerms, filter);
      expect(result).toEqual(expected);
    });
  });

  it('should throw error when document type not known', () => {
    const filter = {};
    const specialSearchTerms = {site: 'example.com'};
    expect(() => searchUtils.setSpecialSearchTermsFilter('unknown', false, '123', specialSearchTerms, filter)).toThrow(Error);
  });
});
Enter fullscreen mode Exit fullscreen mode

Let me explain a bit the signature of test.each(table)(name, fn, timeout):

  • test.each(table) is a Jest function that allows you to define a table of input data to use for parameterized tests.
  • table is an array of arrays, where each sub-array represents a set of input arguments for a test case. Each set of input arguments is used to run the same test function multiple times, with the input arguments substituted.
  • name is a string that describes the test case. It should be unique and descriptive enough to identify the test case in the test results.
  • fn is the test function that takes the input arguments from the table and performs assertions to check if the function under test behaves as expected with those inputs.
  • timeout is an optional parameter that specifies the timeout for the test case. If the test function takes longer than the specified timeout to complete, Jest will mark the test as failed.

Test names

Hold on, regarding the name you said it should be unique and descriptive to identify the test case in the test results...
You are right and the test.each docs offers several possibilities
when setting the name, but one of my favorite ways so far where I have the most control is with the following refactoring
of the test:

describe('setSpecialSearchTermsFilter', () => {
  describe('set the filter correctly', () => {
    test.each([
      [
        'should set the userId filter when specialSearchTerms.userId is present and isPublic is true',
        {}, DocType.SNIPPET, {userId: '123'}, true, '123', {userId: '123'}
      ],
      [
        'should set the userId filter when specialSearchTerms.userId is present and matches the userId',
        {}, DocType.SNIPPET, {userId: '123'}, false, '123', {userId: '123'}
      ],
      [
        'should not set the userId filter when specialSearchTerms.userId is present and does not match the userId',
        {}, DocType.SNIPPET, {userId: '456'}, false, '123', {}
      ],
      [
        'should set the public filter to false when specialSearchTerms.privateOnly is present',
        {}, DocType.SNIPPET, {privateOnly: true}, false, '123', {public: false}
      ],
      [
        'should set the sourceUrl filter when specialSearchTerms.site is present for snippets',
        {}, DocType.SNIPPET, {site: 'example.com'}, false, '123', {sourceUrl: /example.com/i}
      ],
      [
        'should set the location filter when specialSearchTerms.site is present for bookmarks',
        {}, DocType.BOOKMARK, {site: 'example.com'}, false, '123', {location: /example.com/i}
      ]
    ])('%s', (testName, filter, docType, specialSearchTerms, isPublic, userId, expected) => {
      const result = searchUtils.setSpecialSearchTermsFilter(docType, isPublic, userId, specialSearchTerms, filter);
      expect(result).toEqual(expected);
    });
  });

  it('should throw error when document type not known', () => {
    const filter = {};
    const specialSearchTerms = {site: 'example.com'};
    expect(() => searchUtils.setSpecialSearchTermsFilter('unknown', false, '123', specialSearchTerms, filter)).toThrow(Error);
  });
});
Enter fullscreen mode Exit fullscreen mode

In this refactored test suite, each test case has a unique name (testName) which is passed as an argument to the test.each function. The name of the test case is interpolated into the string %s in the template string, which is used as the first argument to test.each.

This way, Jest can generate unique test names for each test case, and if a test case fails, it will be easier to identify which specific test case failed.

Name setting of jest test was also covered in the other blog post - An easy way to set test name in jest repetitive tests (test.each)

Conclusion

I hope you could grasp through this blog post that by using test.each, you can write concise and maintainable test code, reduce code duplication, and make your test suite easier to read and understand.

In conclusion, if you're not already using parameterized tests in your Jest test suite,
test.each is a great way to get started. It's a powerful tool that can help you write better tests and catch more bugs in your code.

We use a lot of jest test.each tests in backend that supports Codever. Check out the project and see for yourself 👉 https://github.com/CodeverDotDev/codever

Top comments (0)