Skip to content

Instantly share code, notes, and snippets.

@twasink
Created April 2, 2024 21:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save twasink/11e457ce71b7b3c5aacb7d97f526a458 to your computer and use it in GitHub Desktop.
Save twasink/11e457ce71b7b3c5aacb7d97f526a458 to your computer and use it in GitHub Desktop.
Jest and Mocks and React, oh my.
/**
* @jest-environment jsdom
*/
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
/*
* The simplest way to mock an import. The `jest.mock` function takes 3 arguments:
* * the path to the module to import (relative or absolute; either works)
* * a "factory function" that returns the function that the module is meant to provide
* * an `options` argument; this is used in the test to mock out a module that doesn't exist.
*
* This format – where the factory function returns a function - is the equivalent of importing
* a module that uses a default export – the way React modules do. The factory function takes
* no arguments, and can either return a single function, or it can return a map of functions
* – each entry in the map is equivalent to an exported property.
*
* Note that if you provide a factory function, it _must_ be an inline function. That's
* because Jest plays all sorts of transpilation games, and changes the underlying code
* by moving things around; using a variable instead of an inline function results in
* bad syntax, because the variable is decleared and initialised _after_ the `jest.mock` call
*
* (There are other weird transpliation games occuring, BTW; the technical term for this kind
* of transpilation game is 'hoisting')
*
* | When using babel-jest, calls to mock will automatically be hoisted to the top of the code block.
* -- Jest documentation.
*
* Note that this version is actually a stub, not a mock. You can't set expectations
* on it, or track calls, or anything.
*/
jest.mock('./StubComponent', () => () => 'Hello World – Stub Component', { virtual: true });
import StubComponent from './StubComponent';
/*
* This version is an actual mock object (as the factory returns a `jest.fn` result).
* Here, the mock function can be provided an initial implementation, but that's very much optional.
* NB: Also, when used with imports, the initial implementation doesn't seem to work!
* As a result, a simpler syntax is just to use jest.fn()
*/
jest.mock('./MockComponent', () => jest.fn(), { virtual: true });
import MockComponent from './MockComponent';
/*
* So although you can't use a variable as the factory function, you _can_ have that
* function return a variable.
* However - due to the transpliation games – you must declare the variable as a `var`. Using
* `const` or `let` doesnt' work, becuase the jest.mock can (but not always) get moved upwards;
* `const` and `let` variables are only available _after_ the declarations, but `var` variables
* are available across the entire scope they are declared in – they just don't get a value until
* they get assigned.
*/
var definedFunction = () => 'Hello World – Defined Function';
jest.mock('./DefinedFunction', () => definedFunction, { virtual: true });
import DefinedFunction from './DefinedFunction';
/*
* However, you can't use a mock function like this. I suspect it's because the
* `jest.fn` isn't working properly at the time the mock is run; the mock is
* set up as undefined
*/
var mockDefinedFunction = jest.fn(() => 'Hello World – Mock Defined Function');
jest.mock('./MockDefinedFunction', () => mockDefinedFunction, { virtual: true });
import MockDefinedFunction from './MockDefinedFunction';
/*
* Okay, let's try some React. Let's do a component with no properties.
* For this one, we'll have it emit some output - it's a stub.
*/
jest.mock('./MockStubbedReact', () => () => <div>Hello World - Stubbed React</div>, { virtual: true });
import MockStubbedReact from './MockStubbedReact';
/*
* This one is a mock; as a result, there's no implementation needed right now
*/
jest.mock('./MockedReact', () => jest.fn(), { virtual: true });
import MockedReact from './MockedReact';
/*
* You can reference the properties if you want. This approach makes them attributes
* on the generated HTML. Note that the attributes are set using `toString`, so arrays
* and objects don't get processsed very well.
* Of course, you can do what you want here - put it in a loop, or whatever.
*/
jest.mock(
'./MockStubbedWithPropertiesReact',
() => (props) => {
return <div {...props}>Hello World - Stubbed React with Properties</div>;
},
{ virtual: true }
);
import MockStubbedWithPropertiesReact from './MockStubbedWithPropertiesReact';
describe('Experimenting with Mocks', () => {
describe('Regular boring mocks', () => {
// Start with some basic stuff.
test('Straight function', () => {
// The syntax for an inline function
var value = () => 'Hello World';
expect(jest.isMockFunction(value)).not.toBeTruthy();
expect(value()).toEqual('Hello World');
});
test('Simple mock', () => {
// A simple mock function, proving we can call it.
var value = jest.fn(() => 'Hello World Simple');
expect(jest.isMockFunction(value)).toBeTruthy();
expect(value()).toEqual('Hello World Simple');
expect(value).toHaveBeenCalled();
// Note that you can change the implementation
value.mockImplementation(() => 'Hello Other World');
expect(value()).toEqual('Hello Other World');
});
test('function mock', () => {
// A function that returns a mock function – similar to the factory function
// approach used in importing modules. Note that _this_ seems to work.
var value = () => jest.fn(() => 'Hello World Function');
expect(jest.isMockFunction(value)).not.toBeTruthy();
var valueFn = value();
expect(jest.isMockFunction(valueFn)).toBeTruthy();
expect(valueFn()).toEqual('Hello World Function');
expect(valueFn).toHaveBeenCalled();
});
});
describe('Imported mocks', function () {
test('Imported mock with simple return', () => {
var value = StubComponent();
expect(value).toEqual('Hello World – Stub Component');
expect(jest.isMockFunction(StubComponent)).not.toBeTruthy();
// StubComponent is a stub, not a mock. As such, we can't check if it's been called.
// expect(StubComponent).toHaveBeenCalled();
});
test('Imported mock with mock return', () => {
expect(jest.isMockFunction(MockComponent)).toBeTruthy();
expect(MockComponent).not.toHaveBeenCalled();
var value = MockComponent();
// There's no implementation, so MockComponent isn't returning anything when called.
expect(value).not.toBeDefined(); // I really wish it was defined.
expect(MockComponent).toHaveBeenCalled();
// But we can give it an implmentation if we want.
MockComponent.mockImplementation(() => 'Hello World – Mock Component');
expect(MockComponent()).toEqual('Hello World – Mock Component');
});
test('Imported mock with defined function', () => {
var value = DefinedFunction();
expect(value).toEqual('Hello World – Defined Function');
expect(jest.isMockFunction(DefinedFunction)).not.toBeTruthy();
// DefinedFunction is a stub, not a mock
// expect(DefinedFunction).toHaveBeenCalled();
});
test('Imported mock with mock defined function – DOES NOT WORK', () => {
expect(jest.isMockFunction(MockDefinedFunction)).not.toBeTruthy();
// in fact, it's not even set up properly
expect(MockDefinedFunction).not.toBeDefined();
// while the other functions are.
expect(DefinedFunction).toBeDefined();
expect(MockComponent).toBeDefined();
});
});
describe('Reactive Mocking', function () {
test('Rendering inline component', function () {
const view = render(<div>Hello World - Inline</div>);
expect(view.container.innerHTML).toEqual('<div>Hello World - Inline</div>');
});
test('Rendering stubbed component', function () {
const view = render(<MockStubbedReact />);
expect(view.container.innerHTML).toEqual('<div>Hello World - Stubbed React</div>');
});
test('Rendering stubbed component with properties', function () {
const view = render(<MockStubbedWithPropertiesReact p1={2} p2={3} />);
expect(view.container.innerHTML).toEqual('<div p1="2" p2="3">Hello World - Stubbed React with Properties</div>');
});
test('Rendering mocked component', function () {
const view = render(<MockedReact />);
expect(MockedReact).toHaveBeenCalled();
// There's no implementation, so there's no inner HTML for the component.
expect(view.container.innerHTML).toEqual('');
// But we can give it an implmentation if we want...
MockedReact.mockImplementation(() => <div>Hello World - Mocked React</div>);
view.rerender(<MockedReact />);
expect(view.container.innerHTML).toEqual('<div>Hello World - Mocked React</div>');
});
// You can also test properties using the expectations provided by Jest.
// Note, however, that you need to provide the extra object in the
// `toHaveBeenCalledWith` – the second object is the React context
// (if one has been set up)
test('Rendering mocked component with properties', function () {
const view = render(<MockedReact p1={2} p2={3} />);
expect(MockedReact).toHaveBeenCalledWith({ p1: 2, p2: 3 }, {});
// There's no implementation, so there's no inner HTML for the component.
expect(view.container.innerHTML).toEqual('');
});
});
});
@twasink
Copy link
Author

twasink commented Apr 2, 2024

There are other mocking techniques I've left out of here (for example, you can provide a default mock for a module by creating a file with the same name inside a __mocks__ folder with the same import path - e.g. path/to/module/__mocks__/modulename). This is not intended to be a complete list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment