Skip to content

Instantly share code, notes, and snippets.

@AaradhyaSaxena
Last active November 3, 2022 10:53
Show Gist options
  • Save AaradhyaSaxena/8e0aa2e172c6aeaa36248af6e63b3f16 to your computer and use it in GitHub Desktop.
Save AaradhyaSaxena/8e0aa2e172c6aeaa36248af6e63b3f16 to your computer and use it in GitHub Desktop.
Jest

Jest-Introduction

Mock functions, also known as spies, are special functions that allow us to track how a particular function is called by external code.

By using mock functions, we can know the following:

  • The number of calls it received.
  • Argument values used on each invocation.
  • The “context” or this value on each invocation.
  • How the function exited and what values were produced.
  • We can also provide an implementation to override the original function behavior. And we can describe specific return values to suit our tests.

When to Use Mock Functions

We can use mock functions when

  • we want to replace a specific function return value.
  • when we want to check if our test subject is executing a function in a certain way.

We can mock a standalone function or an external module method, and we can provide a specific implementation.

For example:

Let's say you are testing a business logic module that uses another module to make requests to an external API. You can then mock the functions of the dependency to avoid hitting the API on your tests. You can then run your tests, knowing what the mocked functions will return to your test subject.

The fact that we can provide an implementation to external dependencies is useful because it allows us to isolate our test subject. We can focus on it. The unit tests would focus entirely on the business logic, without needing to care about the external API.

How to Use jest.fn

There are several ways to create mock functions.

  • The jest.fn method allows us to create a new mock function directly.
  • If you are mocking an object method, you can use jest.spyOn.
  • If you want to mock a whole module, you can use jest.mock.

Higher-order functions are functions that operate on other functions. Either by receiving them as arguments or returning them as values. In the previous example, we can say that greetWorld is a higher-order function, because it expects a function as an input argument

Functions Are First-class Citizens In JavaScript, functions can be treated just like any value. You can pass them as arguments to other functions, they can be assigned as properties of objects (as methods), or you can return other functions from them. Internally, functions are just special objects that can you can invoke.

jest.fn

The simplest way to create a mock function. This method can receive an optional function implementation, which will be executed transparently.

It means that running the mock will work just as if you were invoking the original function implementation. Internally jest.fn will track all the calls and will perform the execution of the implementation function itself.

Example Implementation

function greetWorld(greettingFn) {
  return greetingFn('world');
}

test('greetWorld calls the greeting function properly', () => {
  const greetImplementation = name => `Hey, ${name}!`;
  const mockFn = jest.fn(greetImplementation);
  const value = greetWorld(mockFn);
  expect(mockFn).toHaveBeenCalled();
  expect(mockFn).toHaveBeenCalledWith('world');
  expect(value).toBe('Hey, world!');
});

In this test:

  • We are passing a mock function to the greetWorld function.
  • This mock function has an implementation, which is called internally.
  • The act of passing a mock function to greetWorld allows us to spy on how it uses the function.
  • We expect to have the function to be called one time with the 'world' string as the first argument.

How Does It Work?

The jest.fn method is a higher-order function. It's a factory method that creates new, unused mock functions.

Also, functions in JavaScript are first-class citizens. Each mock function has some special properties.

The mock property is fundamental. This property is an object that has all the mock state information about how the function was invoked. This object contains three array properties:

  • Calls
  • Instances
  • Results

In the calls property, it will store the arguments used on each call. The instances property will contain the this value used on each invocation. And the results array will store how and with which value the function exited each invocation.

There are three ways a function can complete:

  • The function explicitly returns a value.
  • The function runs to completion with no return statement (which is equivalent to returning undefined).
  • The function throws an error.

In the results property, Jest stores each result of the function as objects that have two properties: type and value. Type can be either 'return' or 'throw'. The value property will contain the return value or the error thrown.

If we test the result from within the mock implementation itself, the type will be 'incomplete' since the function is currently running.

Jest provides a set of custom matchers to check expectations about how the function was called:

expect(fn).toBeCalled()
expect(fn).toBeCalledTimes(n)
expect(fn).toBeCalledWith(arg1, arg2, ...)
expect(fn).lastCalledWith(arg1, arg2, ...)

They are just syntax sugar to inspect the mock property directly.

jest.fn Implementation

Let's start with only tracking the arguments used on each call:

// 1. The mock function factory
function fn(impl) {
  // 2. The mock function
  const mockFn = function(...args) {
    // 4. Store the arguments used
    mockFn.mock.calls.push(args);
    return impl(...args); // call the implementation
  };
  // 3. Mock state
  mockFn.mock = {
    calls: []
  };
  return mockFn;
}

If we want to implement the other two features, to track the this value of each invocation and the results of the function, we need to change a couple of things:

  1. We need to add the instances and results arrays to our mock state object:
  // 3. Mock state
  mockFn.mock = {
    calls: [],
    instances: [],
    results: [],
  };
  1. We need to change how we call the mock implementation, to pass the right this value:
//...
  const mockFn = function(...args) {
    // 4. Store the arguments used
    mockFn.mock.calls.push(args);
    mockFn.mock.instances.push(this);
    return impl.apply(this, args); // call impl, passing the right this 
  };
//...

The Function.prototype.apply method allows us to set the this value and apply the arguments array.

  1. We need to wrap the implementation function call in a try-catch statement to know if it throws.

Final Implementation:

// 1. The mock function factory
function fn(impl = () => {}) {
  // 2. The mock function
  const mockFn = function(...args) {
    // 4. Store the arguments used
    mockFn.mock.calls.push(args);
    mockFn.mock.instances.push(this);
    try {
      const value = impl.apply(this, args); // call impl, passing the right this
      mockFn.mock.results.push({ type: 'return', value });
      return value; // return the value
    } catch (value) {
      mockFn.mock.results.push({ type: 'throw', value });
      throw value; // re-throw the error
    }
  }
  // 3. Mock state
  mockFn.mock = { calls: [], instances: [], results: [] };
  return mockFn;
}

jest.spyOn

If we want to make sure that our tests are idempotent, we need to restore the mocked function to its original value. With the mock implementation described above, we have to keep the original value and then reapply at the end of the test.

function fn(impl = () => {}) {
  const mockFn = (...args) => {
    mockFn.mock.calls.push(args)
    return impl(...args)
  }
  mockFn.mock = {calls: []}
  mockFn.mockImplementation = newImpl => (impl = newImpl)
  return mockFn
}

and

function spyOn(obj, prop) {
  const originalValue = obj[prop]
  obj[prop] = fn()
  obj[prop].mockRestore = () => (obj[prop] = originalValue)
}

Test function

We need to be able to write a test implementation, give it a title, and flag if it went well or not.

test('Passing test', () => {})
   // ✓ Passing test
test('Failing test', () => new Error('Error message'))
   // ✕ Failing test
   // Error message

So we can implement:

function test(title, callback) {
  try {
    callback()
    console.log(`✓ ${title}`)
  } catch (error) {
    console.error(`✕ ${title}`)
    console.error(error)
  }
}

Expect

Now we need to be able to assert the values we want to test. Example: toBe function.

We will test the following function:

function multiply(a, b) {
   return a * b
}

And we want to test:

test('Multipling 3 by 4 is 12', () => {
   expect(multiply(3, 4)).toBe(12)
}) // ✓ Multipling 3 by 4 is 12
test('Multipling 3 by 4 is 12', () => {
   expect(multiply(3, 4)).toBe(13)
}) // ✕ Multipling 3 by 4 is 12
   // Expected: 13
   // Received: 12

Implementation:

function expect(current) {
  return {
    toBe(expected) {
      if (current !== expected) {
        throw new Error(`
          Expected: ${expected}
          Received: ${current}
        `)
      }
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment