~/techulus

Thoughts, experiments and ideas.

Unit testing JS generator functions in Jest

// Written by Arjun Komath

// Sat, Jan 27 2024

Testing generator functions was a bit of a puzzle for me. I searched high and low for resources to guide me through it, but came up empty-handed. So, I took matters into my own hands and decided to write something myself.

Throughout this article, we’ll be crafting multiple unit tests for the following code:

export function* world() {
  return "world";
}

export function* hello() {
  const result = ["hello"];
  result.push(yield world());
  return result.join(" ");
}

Testing Hello World

Consider this straightforward example for testing a hello() function that yields results from world(). What’s intriguing is that our test function is also a generator function. This approach allows us to sidestep the need to create an iterator for iterating through the values generated by hello().

test("hello world", function* () {
  const message = yield hello();
  expect(message).toEqual("hello world");
});

In a more realistic setup, you’ll find yourself testing a lot more scenarios, such as handling failures, verifying the number of calls made, and ensuring the returned data meets expectations.

Mocking nested yield

In this example, we’re going to simulate a nested yield to mimic a real-world scenario where we’re interacting with an external system. This approach enables us to test various scenarios where different values are returned as side-effects.

const { hello } = require("../dist/hello");
const { world } = require("../dist/world");

jest.mock("../dist/world");

test("hello world", function* () {
  world.mockReturnValue(function* () {
    return "something else";
  });

  const message = yield hello();
  expect(world).toHaveBeenCalledTimes(1);
  expect(message).toEqual("hello something else");
});

Testing failures

This is a bit peculiar. I encountered an issue where I couldn’t get Jest’s error testers to cooperate. Instead, I opted for wrapping the calls within a try-catch block and then anticipating the error. I’m not entirely convinced this is the optimal approach. Let me know if you have any ideas for improvement.

test("hello world", function* () {
  world.mockReturnValue(function* () {
    throw "Oops!";
  });

  try {
    yield hello();
  } catch (e) {
    expect(e).toBe("Oops!");
  }
});