How to Add Custom Functions to a Jest Test Suite

Article summary

Jest is a fantastic testing framework. Of the many I’ve used across different platforms and languages, it’s the best by a landslide. Jest is fast to learn, easy to use, full of features out of the box, and simple to customize.

One of the opportunities to customize your Jest test suite is to add custom functions to the test helpers. The Jest docs list several configuration options, and we can use those to our advantage while customizing our Jest test suite.

The Goal

I want easy-to-use, discoverable functions to add to the test suite. I don’t want to change much syntax from vanilla Jest, and I don’t want to import anything. Consider the following test:

 
describe("my test suite", () => {
  it("does something", () => {
    expect(1).toEqual(1);
  });
});

Say that the test is flakey, and I want to run it multiple times. Or there could be some code in the beforeEach or afterEach that could change the state of the test. Either way, it would be convenient to write the following to quickly debug it:

import { itRepeats } from "test/helpers";

describe("my test suite", () => {
  itRepeats(10, "does something", () => {
    expect(1).toEqual(1);
  });
  // -- or --
  itRepeats(10)("does something", () => {
    expect(1).toEqual(1);
  });
});

This pattern of testing has some drawbacks that I’d like to avoid. Preferably, I would write something like this:

describe("my test suite", () => {
  it.repeats(10, "does something", () => {
    expect(1).toEqual(1);
  });
  // -- or --
  it.repeats(10)("does something", () => {
    expect(1).toEqual(1);
  });
});

This would fit two of my goals: I don’t want the syntax of this helper to deviate far from Jest, and I don’t want to import any additional helper functions. Jest has some configuration options that make it pretty simple to implement this.

Configuring Jest

The Jest documentation specifies that the configuration could be added to jest.config.js, jest .config.json, or in the package.json, but for this example, we’ll use the jest.config.js.

Reading through the configuration options, we see the property setupFilesAfterEnv, which states:

A list of paths to modules that run some code to configure or set up the testing framework before each test. Since setupFiles executes before the test framework is installed in the environment, this script file presents you the opportunity of running some code immediately after the test framework has been installed in the environment.

Because it runs some code after Jest has been installed in the environment, we can use this to our advantage to customize (i.e. monkey-patch / duck-punch) Jest.

Add the following to your jest.config.js:

module.exports = {
  // Other configuration above...

  // Add the next three options if using TypeScript
  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
  preset: "ts-jest",
  transform: {
    "^.+\\.tsx?$": "babel-jest",
  },

  // Run these files after jest has been
  // installed in the environment
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], // use .js if you prefer JavaScript
};

Note: If you’ve configured Enzyme for Jest, then you may have already have done something like this before. Feel free to create a new file or reuse the same one.

At the root of the project, create a new file called jest.setup.ts. This file will be run after Jest has been installed into the environment. We can add custom functionality to Jest here:

test.repeats = async (
  times: number,
  name: string,
  fn?: jest.ProvidesCallback,
  timeout?: number,
) => {
  await Promise.all(
    Array(times)
      .fill(undefined)
      .map((_, i) => {
        return test(name, fn, timeout);
      }),
  );
};

If you’re using TypeScript, you might get a type error similar to Property 'repeats' does not exist on type 'It'.. We can fix that!

Create another file at the root of your project called jest.d.ts, and add the following to it:

/// <reference types="jest" />

declare namespace jest {
  interface It {
    repeats: (
      times: number,
      name: string,
      fn?: ProvidesCallback,
      timeout?: number,
    ) => void;
  }
}

Then, add the jest.d.ts line to the include property in your tsconfig.json:

{
  ...
  "include": [
    "jest.d.ts",
  ]
}

Now, the type errors should have gone away in the jest.setup.ts, and you should get type completion when you’re writing a `test` or `it` statement in your Jest test.

In this example, we added functionality to repeat a test multiple times. However, this is a bit of a contrived example, to show how everything gets wired up. In reality, we can customize Jest far beyond this.

The above code can be found here.

 
Conversation
  • Manoj says:

    Hey Dan,

    This is an interesting article, I am kind of looking at a similar solution to define the missing API from Jasmine lib `createSpyObj(..)` into Jest lib and I followed the same steps as you did.

    however, I am getting an error in `jest.setup.ts` that the new method (createSpyObj) doesnt exist.

    **jest.setup.ts:**
    jest.createSpyObj = (object: string, methods?: string[], properties?: any) => {
    const obj: any = {};

    for (const method of methods) {
    obj[method] = jest.fn();
    }

    for (const key of Object.keys(properties)) {
    obj[key] = jest.fn().mockReturnValue(properties[key]);
    }

    return obj;
    };

    **jest.d.ts:**
    ///
    declare namespace jest {
    /**
    * Creates a mock function for all the methods or properties of an the object.
    */
    function createSpyObj(object: string, methods?: string[], properties?: any): T;
    }

    i added `jest.setup.ts` is added to `”setupFilesAfterEnv”:[…]` & `jest.d.ts` to `tsconfig.json`

    any quick help would be really appreciated :)

    • Chris says:

      I was able to get it working by wrapping the declaration with `declare global`. So your file would look something like:

      “`
      **jest.d.ts:**
      ///

      declare global {
      namespace jest {
      function createSpyObj(object: string, methods?: string[], properties?: any): T;
      }
      }
      “`

  • Hi Dan:

    Do you happen to have any advice on how one might go about making a top-level function within the Jest environment? For example, let’s say I wanted my test to have the following structure:
    “`
    describe(‘a system under test’, () => {
    scenario(‘when it works correctly’, () => {

    }, /* times to run, similar to repeats */ iterations, /* method to use to accumulate iterations, an enum */, ‘average’);
    });
    “`

    In this example, `scenario` is a new, top level function, similar to `it`, `test`, or `context`, with the following parameters:
    “`
    scenario(name: string, fn: ProvidesCallback, iterations: number, measureMethod: enum) => void;
    “`

    I can’t seem to get my project under tests to recognize this in the same way you did. Thoughts on how you might go about this?

    • @Manoj: Sorry, I posted before I saw your answer. This is, in essence, what I’m trying to do. However, when I use your strategy, I get the error in my `jest.setup.ts` file:
      “`
      propery ‘scenario’ does not exist on type ‘typeof jest’
      “`

      Perhaps I’m incorporating my definition file incorrectly?

      • Dan Kelch Dan Kelch says:

        Hi Scott, thanks for the comment.

        As far as I know, if you want to declare scenario globally, you would need to hook into the global object, like:

        
        // jest.setup.ts
        global.scenario = async (
          name: string,
          fn: jest.ProvidesCallback,
          times: number,
          measureMethod: MeasureMethod,
          timeout?: number
        ) => {
          console.log(
            `Running scenario ${name}: ${times} times, measuring with ${measureMethod}`
          );
          return test(name, fn, timeout);
        };
        

        Note: this may require adding @types/node to your project, and node to your tsconfig, like

        {
          "compilerOptions": {
        ...,
            "types": ["jest", "node"],
          }
        }
        

        Then, you can update the types like so:

        /// <reference types="jest" />
        
        declare var scenario: jest.scenario;
        declare namespace jest {
          interface scenario {
            (
              name: string,
              fn: jest.ProvidesCallback,
              times: number,
              measureMethod: "foo" | "bar",
              timeout?: number
            ): void;
          }
        }
        

        Alternatively, you could use the same method as in the post, and just place scenario under the It interface:

        // jest.setup.ts
        
        test.scenario = async (
          name: string,
          fn: jest.ProvidesCallback,
          times: number,
          measureMethod: MeasureMethod,
          timeout?: number
        ) => {
          console.log(
            `Running scenario ${name}: ${times} times, measuring with ${measureMethod}`
          );
          return test(name, fn, timeout);
        };
        

        And in the jest.d.ts:

        /// <reference types="jest" />
        
        declare namespace jest {
          interface It {
            repeats: (
              times: number,
              name: string,
              fn?: ProvidesCallback,
              timeout?: number
            ) => void;
            scenario: (
              name: string,
              fn: jest.ProvidesCallback,
              times: number,
              measureMethod: "foo" | "bar",
              timeout?: number
            ) => void;
          }
        }

        So you would just need to use the method like:

        
        describe("a system under test", () => {
          it.scenario(
            "when it works correctly",
            () => {
              expect(1).toBe(1);
            },
            /* times to run, similar to repeats */ iterations,
            /* method to use to accumulate iterations, an enum */ "foo"
          );
        });
        
        

        I haven’t tested this myself, but I believe both options would work. Hope this helps!

        • Thanks, Dan! I appreciate your quick and thorough response. On a side note, how did you get the code to look right under your comment post? (Mine looked pretty bunched up and unformatted, so just for future reference, I’d like to edit it so it’s readable)

          • Dan Kelch Dan Kelch says:

            No problem! Hopefully you were able to resolve your questions.

            I had the same question regarding comments, in fact! My colleague Kory pointed out that authors can use our blog post editor to write comments, complete with formatting. I’m unsure whether the comment box below the post is able to do the same thing – but that would certainly be a handy feature.

            Cheers!

  • Comments are closed.