Jest My Summer — Approaching Various Problems With Jest + Enzyme
Updated Aug 11/17
Like many interns, this summer I will be unit testing code. Since we have reached the half way point of summer, I would like to share some of the lessons I have learned, in hopes that they will save others time and frustration.
Since no one reads this part anyways, let’s get into it.
Disclaimer: The following are solutions to various problems that I face on a daily basis. There are likely better ways to solve these problems, but I have found that these solutions work (but if you know better ways, feel free to tell me).
Environment..
My workplace uses React and TypeScript, because of this, the lessons I have learned are tailored to these topics.
Packages I use include..
Jest
https://facebook.github.io/jest/docs/en/getting-started.html
Enzyme
enzyme-to-json
react-dom
I have used react-test-renderer, however, I am not aware of any advantages to using it over enzyme + enzyme-to-json.
A note about package.json..
To use Jest with Typescript you need to include a preprocessor in your package.json, There are a lot of guides on this, take a look here:
Lets get started..
Overview of Topics# File setup
# Jest provides no output
# Running tests
# Checking equivalency
# Mocking functions
# Async and callbacks
# Directly placing HTML on the DOM
# Setting attributes
# Snapshots with enzyme
# State changes (enzyme)
# Passing events to simulate (enzyme)
# Mocking the implementation of a function call
# Passing variables to mocked implementation
# Mocking a single instance of a function call
# Reference errors
# Testing static methods
# Directly calling methods
# Snapshots+
# Simulating events with enzyme
# Stubbing
# Accessing private methods
# Passing an unknown or variable number of args to mock
# Capturing arguments of a spy call
# Mocking the methods of Object.create(interfaceName)
# Stubbing an undefined class
# Function passed to setTimeout is not executing
File Setup
Your tests need to be in .tsx files!
Jest provides no Output
On new repositories I have ran into the problem of running “npm test” and the tests finishing without any output at all.
If this occurs, you can delete the node modules folder and rebuild all modules (npm install), this should fix the issue.
Running Tests
If you only want to run a single test you can use:
test.only(‘Test Name’, () => { // test code});
If you have multiple test.only’s, only the tests with a .only will be ran. test.skip can be used in a similar manner.
Checking Equivalency
Recursively examine the properties of x and y to see if they are equivalent:
expect(x).toEqual(y);Examine if x and y are the same object:
expect(x).toBe(y);Mocking Functions
spyOn() **Great mocking function**
- This is a Jasmine (library) function. It mocks the function that it is given.
spyOn(moduleName, “functionName”);
// Will mock moduleName.functionNameYou cannot spyOn a function that doesn’t belong to a module, however try setting the module as window for “module-less” functions.
- Certain functions belong to a prototype module (React classes work this way):
spyOn(HTMLElement.prototype, ‘dispatchEvent’);
spyOn(YourReactComponent.prototype, ‘methodName’);- Others (such as many jQuery functions) work differently still:
spyOn($.fn, ‘animate’);- A common expect statement after mocking:
expect(YourReactComponent.prototype.methodName).toHaveBeenCalled();- By default, spyOn overrides the original implementation. To retain the original implementation use:
spyOn(x, ‘y’).and.callThrough();- To input custom return values you can use:
spyOn(x, ‘y’).and.returnValues(1, ‘abc’, ‘thirdReturn’, [1, 2, 3]);
// Returns each of the values once (in order)spyOn(x, ‘y’).and.returnValue(3);
// Will always return 3
- spyOn lets us use a lot of useful Jest functions such as:
expect(x.y).toHaveBeenCalled();
expect(x.y).not.toHaveBeenCalled();
expect(x.y).toHaveBeenCalledWith(args);
expect(x.y).toHaveBeenCalledTimes(number);- Note: you will find online references to spyOn calls being performed before all other code. Through my own testing I have not found this to be true.
For the following example, the first addClass method gets executed with the native implementation, and the second does not due to the spyOn mocking:
document.body.innerHTML = ‘<div id=”container”></div>’;
$(‘#container’).addClass(‘new_1’);spyOn($.fn, ‘addClass’);
$(‘#container’).addClass(‘new_2’);
- Note: you cannot spyOn the same method twice. If you attempt this, you will receive an error when you run the tests with Jest.
jest.fn() **An ok mock function**
- Put in place of a function — This function is useful when passing functions as arguments. You can pass a reference to jest.fn() instead to insert a mock into a part of the existing code.
- Capture args passed to a function — Good for targeting a specific argument passed to the function. The args are contained in 2D arrays of the form: (call_number x args_for_call)
let mockFn = jest.fn();mockFn(1, ‘a’);
mockFn(2, ‘b’);/*
mockFn.mock.calls[0] => [1, ‘a’]
mockFn.mock.calls[0][1] => ‘a’
mockFn.mock.calls[1] => [2, ‘b’]
*/
Async and Callbacks
Dealing with async functions requires callbacks. A typical test function using callbacks has the form:
test(‘testName’, (cb: any) => {
let a = asyncFunctionCall(); setTimeout(() => {
expect(a).toBe(b);
cb();
}, 60);});
Directly Placing HTML on the DOM
When working with creating elements on the DOM, you can directly add html using:
document.body.innerHTML = ‘<div id=”container”></div>’;If you want to mount a component onto the DOM, pass the identifier of an element on the DOM to the enzyme mount function:
let ENZYME_RENDER = Enzyme.mount(<MyComponent />, '#container');Mounting your component to the DOM is important if you use the react-dom npm package, since it searches the DOM directly.
Setting Attributes
Sometimes it is difficult to directly set an attribute when a setter does not exist. To get around this you can use:
Object.defineProperty(
$(‘#identifier’)[0],
‘attributeName’,
{ value: "setToValue", writable: true }
);Ensure that you have the writable: true option, otherwise this property becomes immutable.
If you cannot set an attribute, you can try to mock the method used to get that attribute.
For instance, I had trouble setting a value for $(‘#element’).offset().top
My solution:
spyOn($.fn, ‘offset’).and.returnValue({ top: 1 });Snapshots with Enzyme
Use enzyme-to-json (npm package) to deal with creating snapshots from enzyme wrappers.
Enzyme is great for simulating events and also for snapshots with this package
There should be configuration for this package in package.json for Jest, so you need only to pass an enzyme wrapper and it will create a snapshot:
expect(Enzyme.mount(<YourReactComponent />).toMatchSnapshot();State Changes (enzyme)
Enzyme has many functions to help with state changes such as: setProps() and setState()
Passing Events to Simulate (enzyme)
When simulating events with enzyme pass event objects to the handler functions as follows:
ENZYME_RENDER.find(‘#identifier’).simulate(
‘click’,
{prop1: val1, prop2: val2}
);Mocking the implementation of a function call
If you want to alter a method, you can create a mock with the new implementation, using spyOn:
let mock = jest.fn().mockImplementation(() => {
console.log(‘Mock was called’);
});spyOn(Module, ‘MethodName’).and.returnValue( mock() );
Passing variables to Mocked Implementation
If you would like to mock the implementation of a function but still utilize the variables that were passed to the function,
$(…).find(…).each((x: any, y: any) => {…}) example above: we would like to use both x and y in our mock function
You can proceed using:
let mockFn = {
each: jest.fn().mockImplementation(
(param1: any, param2: any) => {
console.log(param1, param2)
}
)
}spyOn($.fn, ‘find’).and.returnValue(mockFn);
This allows us to grab the original implementation, pass custom variables to it, or alternatively completely modify the way in which the .each() function is called.
Mocking a single instance of a function call
If you want to only mock the first occurrence of a method, and then use the original implementation afterwards you can do so in the following way:
let firstReturn: any = { returnValue: “anything” };const temp = $;
let first = true;spyOn(window, ‘$’).and.callFake(function (param: any) { if (first) {
first = false;
return firstReturn;
} else {
return temp(param);
}});
The above allows you to return a mock value for the first call to jquery, and then for all subsequent calls you call the original jquery implementation, taking in the parameters that were passed in the function.
Reference Errors
When you run into reference errors, you can get around this by globally defining variables as follows:
Object.defineProperty(
window,
‘variableName’,
{ value: {attr1: ‘a’, attr2: ‘b’}, writable: true }
);remember: “writable: true” makes it mutable!
Testing Static Methods
TypeScript won’t let you directly test these (this will result in errors that the function does not exist on the module causing build errors).
To get around this, you can just create a copy of your instance as such:
import importedModule = require(‘pathToModule/module’);let moduleCopy: any = importedModule;
expect(moduleCopy.staticFn()).toMatchSnapshot();
In this example we assume that the staticFn returns a map that we can store in a snapshot.
Directly calling methods
If you want to directly call a method of a React class you need to get a pointer to the component itself from your enzyme wrapper (which must be mounted!).
let ENZYME_RENDER: any = Enzyme.mount(<YourReactComponent />);expect(ENZYME_RENDER.getNode().publicMethod()).toBe(42);
Snapshots+
Snapshots don’t need to be used for just React elements. They are great ways to store and compare return values (especially long JSON return values).
Simulating Events with Enzyme
Enzyme requires a single node to simulate an event. To get a single node from a list of many proceed as follows:
ENZYME_RENDER.find(‘li’).at(1).simulate(‘click’);This will perform a click event on the second element.
Stubbing
Sometimes when items are not available, we must create complicated stubs.
For example, lets say that mFunction is not defined in the following statement:
mFunction.GetWindow().PropertyName.Fn(testEnum.pos);We proceed as follows:
let mock = jest.fn();
Object.defineProperty(
window,
‘mFunction’,
{ value:
{ GetWindow: () => { return { PropertyName: { Fn: mock } } } }
}
);Accessing Private Methods
If you want to test private methods which are not explicitly called, but are passed as props to a child component you can do so as follows:
ENZYME_RENDER.find(‘Button’).at(0).props().onClick();This will find the first Button child component, return the list of props that was passed to it and call the private method that was passed as the onClick prop.
Capturing Arguments of a Spy Call
This is one of the most useful features I have found, we use the Jasmine CallTracker to grabs our args:
spyOn(ModuleName, ‘functionName’);Which enables us to use:
expect(ModuleName.functionName.calls.allArgs()[0])
.toEqual([‘firstParam’, ‘secondParam’]);Alternatively, we can feed the jasmine output into a jest mock function:
let mock = jest.fn();
spyOn(ModuleName, ‘functionName’).and.callFake(
function (…param: any[]) {
mock(param);
}
);expect(mock.mock.calls[0]).toEqual([‘firstParam’, ‘secondParam’]);
Note: The above jest.fn() method is useful when the .calls parameter appears to be undefined on your spied on function.
Mocking the methods of Object.create(interfaceName)
Since our object was created from an interface, we cannot directly mock the methods in our object (the new object doesn’t exist before runtime, and we cannot mock the method of an interface).
This workaround requires the Object.create(…) method to be retrievable.
There are many ways we can capture this object (as a parameter for a mock), but hopefully it is stored as an instance variable of your component, since this will be easiest.
So if we have a sample component called MyComponent, and the following piece of code as one of it’s lifecycle methods:
componentDidMount() {
...
this.myInstance = Object.create(interfaceName);
...
}Then our test would look like:
test(‘MyComponent componentDidMount’, () => {
let ENZYME_RENDER: any = Enzyme.mount(<MyComponent />);
let mockFn = jest.fn(); spyOn(ENZYME_RENDER.getNode().myInstance,‘methodWeWantToMock’)
.and.callFake(
function (…param: any[]) {
mockFn(param);
}
); ENZYME_RENDER.getNode().componentDidMount(); expect(mockFn.mock.calls[0][0]).toBe(expectedValue);
}
So what we are doing is generating the object, and then retroactively spying on the method. We then need to re-execute the method for the mock to take-in values.
Stubbing an Undefined Class
To mock a call to the constructor of an undefined class, something that would look like:
let ClassObject = new MyUndefinedClass();we create a stub as a stand in:
class MyUndefinedClassSTUB {
constructor() {} // any functions you require
}Object.defineProperty(window, ‘MyUndefinedClass’, {
value: MyUndefinedClassSTUB
});
Function passed to setTimeout is not Executing
Under certain conditions, the function parameter passed to setTimeout is not executed. If we need to test this method we still can.
What we do is spyOn the setTimeout method, in order to grab and execute the method that was passed as an argument.
In this example we assume that a setTimeout occurs in the componentDidMount method for our component, and we are having trouble executing it.
let mockFn = jest.fn();
spyOn(window, ‘setTimeout’).and.callFake(
function (…param: any[]) {
mockFn(param);
}
);let EVENT_RENDER: any = Enzyme.mount(<MyComponent />);
mockFn.mock.calls[0][0]();
In this way we will execute the function passed to setTimeout.
That’s everything.
Unit testing the front end of such a large code base has taught me a lot of lessons and I can truly say that although frustrating at times, I’m happy to have had the experience. If you care for numbers the final tally was:
- 71 test suites
- 470 snapshots
- 753 tests
I hope that there are things in this article you can use in your own work, and wish you good luck!
