Created
April 2, 2024 21:51
-
-
Save twasink/11e457ce71b7b3c5aacb7d97f526a458 to your computer and use it in GitHub Desktop.
Jest and Mocks and React, oh my.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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(''); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.