Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to nest Jest tests

I have a situation where I need to create a test that calls a function and checks its return value. It returns an object so I have to iterate through it and check 100 or so values for correctness. If one of them fails I want to know which one.

I cannot work out how to do this with vanilla Jest such that the test is self-contained and I get a meaningful error message on a failure.

For example, I can do this: (pseudocode to illustrate, not actual code)

describe('Test for function A', () => {
    beforeAll('Create class instance', () => {
        this.inst = new MyClass();
    });

    test('Call function with no parameters', () => {
        const value = this.inst.run();

        for (each key=value) {
            expect(value).toBe(correct); // on failure, doesn't tell me the key, only the value
        }
    });
});

The problem with this is that if value is not correct then the error message is not very helpful, as it doesn't tell me which of the 100 values has the problem.

I can't change to test.each() because then I get an error saying I have nested test() calls which is not allowed.

If I use an inner test() and change the parent test() to describe() then the code becomes this:

describe('Test for function A', () => {
    beforeAll('Create class instance', () => {
        this.inst = new MyClass();
    });

    describe('Call function with no parameters', () => {
        const value = this.inst.run();

        for (each value) {
            test(`Checking ${value.name}`, () => {
                expect(value).toBe(correct);
            });
        }
    });
});

This would give me a detailed error message except this.inst.run() is called during test set up, before this.inst has been set by beforeAll(), so it fails. (Jest runs all the describe() blocks first, then beforeAll(), then test(). This means I call this.inst.run() first in a describe() block before the beforeAll() block creates the class instance.)

Is there any way that this is possible to achieve? To have a test that requires an object created and shared amongst all the child tests, a test group that calls a function to get data for that group, then a bunch of tests within the group?

like image 342
Malvineous Avatar asked Mar 03 '21 08:03

Malvineous


People also ask

Is Jest enough for testing?

Jest is a JavaScript test runner that lets you access the DOM via jsdom . While jsdom is only an approximation of how the browser works, it is often good enough for testing React components.

Do Jest tests run sequentially?

Jest will execute different test files potentially in parallel, potentially in a different order from run to run. Per file, it will run all describe blocks first and then run tests in sequence, in the order it encountered them while executing the describe blocks.

Is Jest better than karma?

Jest is 2 to 3 times faster than karma testing The tests that took 4–5 minutes on KARMA only takes about 1–2 minutes on jest. This is particularly important when using CI-CD ( Continous Integration/Continous Delivery). Since the tests are faster the execution time of CI-CD will also reduce.


2 Answers

Yes, it is possible according to the order of execution of describe and test blocks:

describe("Test for function A", () => {
  this.inst = new MyClass();
  afterAll("Create class instance", () => { //--> use this instead of beforeAll
    this.inst = new MyClass();
  });

  test("Should be defined", () => {
    //--> at least one test inside describe
    expect(inst).toBeTruthy();
  });

  describe("Call function with no parameters", () => {
    const value = this.inst.run();

    test("Should be defined", () => {
      //--> at least one test inside describe
      expect(value).toBeTruthy();
    });

    for (/*...each value */) {
      test(`Checking ${value.name}`, () => {
        expect(value).toBe(correct);
      });
    }
  });
});
like image 189
lissettdm Avatar answered Oct 16 '22 11:10

lissettdm


I came up with a workaround for this. It's a bit hacky but it seems to work. Essentially you use promises to wrap the value you're interested in, so one test will sit there await-ing the result from another test.

Obviously this will only work if the tests are run in parallel, or if the sequential ordering is such that the promise is resolved before it is awaited.

The only trick below is that the await is placed in a beforeAll() block, so that the value is available to all tests within that describe() section. This avoids the need to await in each individual test.

The benefit of this is that the test set up (creating the object) is within a test() so exceptions are captured, and the checks themselves (expect().toBe()) are in separate tests so that the test name can be set to something descriptive. Otherwise if your expect() calls are in a for loop, when one fails there's no way to figure out which array entry was at fault.

It's a lot of work just because you can't supply a description on the expect() call (unlike other testing frameworks), but if you're stuck with Jest then this does at least work. Hopefully one day they will add a per-expect description to avoid all this.

Here is some sample pseudocode:

describe('Test for function A', () => {
    let resolveValue;
    let promiseValue = new Promise(resolve => resolveValue = resolve);

    describe('Create class instance', () => {
        test('Run processing', () => {
            this.inst = new MyClass();
            // inst.run() is now called inside a test(), so any failures will be caught.
            const value = this.inst.run();
            resolveValue(value); // release 'await promiseValue' below
        });
    });

    describe('Call function with no parameters', () => {
        let value; // this is global within this describe() so all tests can see it.

        beforeAll(async () => {
            // Wait for the above test to run and populate 'value'.
            value = await promiseValue;
        });

        for (each value) {
            // Check each value inside test() to get a meaningful test name/error message.
            test(`Checking ${value.name}`, () => {
                // 'value' is always valid as test() only runs after beforeAll().
                expect(value).toBe(correct);
            });
        }
    });
});
like image 44
Malvineous Avatar answered Oct 16 '22 09:10

Malvineous