Jest: Difference between revisions
(2 intermediate revisions by the same user not shown) | |||
Line 379: | Line 379: | ||
To solve this problem we need to transpile (convert) the module from esm to commonJS. To do this we need to config jest like this. | To solve this problem we need to transpile (convert) the module from esm to commonJS. To do this we need to config jest like this. | ||
<syntaxhighlight lang="js"> | <syntaxhighlight lang="js"> | ||
export default async (): Promise<Config> => { | export default async (): Promise<Config> => { | ||
return { | return { | ||
rootDir: '../', | |||
verbose: true, | verbose: true, | ||
setupFilesAfterEnv: ['<rootDir>/setupTests.ts'], | setupFilesAfterEnv: ['<rootDir>/configs/setupTests.ts'], | ||
testEnvironment: '<rootDir>/jest.environment. | testEnvironment: '<rootDir>/configs/jest.environment.ts', | ||
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], | ||
Line 392: | Line 395: | ||
transform: { | transform: { | ||
'^.+\\.(ts|tsx)?$': 'ts-jest', | '^.+\\.(ts|tsx)?$': 'ts-jest', | ||
'^.+\\.(js|jsx)$': 'babel-jest', | '^.+\\.(js|jsx)$': ['babel-jest', { configFile: './configs/babel.config.js' }], | ||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': | ||
'<rootDir>/__mocks__/fileMock.js', | '<rootDir>/configs/__mocks__/fileMock.js', | ||
'^.+\\.(css|sass|scss)$': '<rootDir>/__mocks__/styleMock.js', | '^.+\\.(css|sass|scss)$': '<rootDir>/configs/__mocks__/styleMock.js', | ||
}, | }, | ||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], | |||
transformIgnorePatterns: ['/node_modules/(?!(uint8array-extras|msal-react-tester)/)'], | transformIgnorePatterns: ['/node_modules/(?!(uint8array-extras|msal-react-tester)/)'], | ||
... | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
The important lines is the transformIgnorePatterns which tells jest to ignore all modules except uint8array-extras or msal-react-tester. I am hopeless at regex so went there to prove it. You can see only the last two are selected and therefore ignored. | The important lines is the transformIgnorePatterns which tells jest to ignore all modules except uint8array-extras or msal-react-tester. I am hopeless at regex so went there to prove it. You can see only the last two are selected and therefore ignored. | ||
[[File:Reg101 image1 jest.png | 400px]] | [[File:Reg101 image1 jest.png | 400px]] | ||
==Other Configs== | |||
There are other configurations involved too and are provided here for completeness | |||
*setupTests.ts | |||
*jest.environment.ts | |||
<syntaxhighlight lang="ts"> | |||
// setupTests.ts | |||
import '@testing-library/jest-dom' | |||
import crypto from 'crypto' | |||
Object.defineProperty(window, 'crypto', { | |||
value: { | |||
randomUUID: () => crypto.randomUUID(), | |||
}, | |||
}) | |||
</syntaxhighlight> | |||
<br> | |||
<syntaxhighlight lang="ts"> | |||
// jest.environment.ts | |||
import Environment from 'jest-environment-jsdom' | |||
export default class CustomTestEnvironment extends Environment { | |||
async setup() { | |||
await super.setup() | |||
this.global.TextEncoder = TextEncoder | |||
this.global.TextDecoder = TextDecoder | |||
this.global.Response = Response | |||
this.global.Request = Request | |||
} | |||
} | |||
</syntaxhighlight> |
Latest revision as of 05:22, 4 December 2024
Introduction
This is a page to introduce Jest. Facebook employs a lot of the people who develop jest and can be and is used for test in other frameworks.
Example code can be found at [here]
Choices
Popular tools for testing are Enzyme and Jasmine/Mocha. Jest is built on top of Jasmine/Mocha. Jest works with or without React and has a great assertion library.
Why Not Enzyme
This has over 200+ issues open and 26 of these are bugs. Jest is recommended by the react team.
Jest vs Mocha
Jest and Mocha both run tests sync and async but jest also has the following features.
- Spies (stubbing methods to spy)
- Snapshot testing
- Module mocking (stubbing objects)
Getting Started
Running
We can run jest on it's own or continually with watch mode
jest --watch
At least one "it" block must be found or jest will return an error.
Code Coverage
With Jest we can get a report by typing
npm test -- --coverage
Naming Test
The file names must be recognized as a test.
__tests__/*.js
*.spec.js
*.test.js
Example Test in Jest
Describe is the suite, it is the test
describe("The question list ", ()=> {
it ("should display a list of items", ()=> {
expect(2+2).toEqual(4);
})
it ("should display a list of items", ()=> {
expect(2+4).toEqual(6);
})
})
Setup and Teardown
This can be done using before each and before all.
BeforeEach BeforeAll
beforeAll(()=>{
console.log("Hello");
});
and to tear down we can use AfterEach AfterAll
afterAll(()=>{
console.log("Hello");
});
Note does not matter what order you write them in.
Skipping and Isolating Tests
Add keyword only to include tests
it.only ("should display a list of items", ()=> {
expect(2+2).toEqual(4);
})
Add keyword skip to exlude tests
it.skip ("should display a list of items", ()=> {
expect(2+2).toEqual(4);
})
Async Tests
// Using a callback that is passed to the test force it to wait
it('async test 1', done=> {
setTimeout(done, 100)
});
// Return a Promise which jest knows to wait for
it('async test 2',()=> {
return new Promise(
resolve => setTimeout(resolve,100)
);
});
// Use await to wait before returning.
it('async test 1', async ()=>
await delay(100)
);
Mocking
Why
- Reduces dependencies and makes testing faster
- Prevents side effects
- Control the scenario we wish to test e.g. timeout, corrupt data
What is Mocking
A mock is a replacement for known functionality which can be used to capture usage of the original function and fake a response to the users of the mock.
Setup
Create directory __mocking__ at the same level as npm node_module you are trying to mock. This makes your module get loaded in place of the original module.
Example
Function to Mock
/**
* Fetch question details from the local proxy API */
export function * handleFetchQuestion ({question_id}) {
const raw = yield fetch(`/api/questions/${question_id}`);
const json = yield raw.json();
const question = json.items[0];
/**
* Notify application that question has been fetched
*/
yield put({type:`FETCHED_QUESTION`,question});
}
Mocking the JavaScript Fetch function
In the code below we are attempting to mock the javascript fetch function where call the function and return a value. I orginally struggled with this because of the old syntax but it became a lot clear after so much work with node js and modern javascript
let __value = 42;
// Old syntax
// Replace a function which returns 42
const isomorphicFetch = jest.fn( (){ returns __value });
// Add a function to set the value
isomorphicFetch.__setValue = function(v) {__value = v};
// Modern notation
const isomorphicFetch = jest.fn( ()=> __value);
isomorphicFetch.__setValue = v => __value = v;
export default isomorphicFetch;
Mocking the handleFetchQuestion
Now we need to mock the original function above using our ownversion of fetch. We set the value to be the format expected back from fetch which is an object which we can do because __setValue is not type aware [{"question_id": 42}]
import { handleFetchQuestion } from './fetch-question-saga';
import fetch from 'isomorphic-fetch';
/**
* This test is an example of two important Jest testing principles,
* 1) we're mocking the "fetch" module, so that we don't actually make a request every time we run the test
* The module, isomorphic fetch, is conveniently mocked automatically be including the file __mocks__/isomorphic-fetch.js adjacent to to the Node.js folder
* 2) we're using an async function to automatically deal with the fact that our app isn't synchronous
*/
describe("Fetch questions saga",()=>{
beforeAll(()=>{
fetch.__setValue([{question_id:42}]);
});
it("should get the questions from the correct endpoint in response to the appropriate action",async ()=>{
const gen = handleFetchQuestion({question_id:42});
/**
* At this point, isomorphic fetch must have been mocked,
* or an error will occur, or, worse, an unexpected side effect!
*/
const { value } = await gen.next();
expect(value).toEqual([{question_id:42}]);
/**
* We can also assert that fetch has been called with the values expected (note that we used a spy in the file where we mock fetch.)
*/
expect(fetch).toHaveBeenCalledWith(`/api/questions/42`);
});
});
Snapshot Testing
It is really hard to be excited about this. It seems so fragile. Basically it records the first run and makes comparisons on subsequent runs. Any change is flagged.
import React from 'react';
import TagsList from './TagsList'
import renderer from 'react-test-renderer';
describe("The tags list",()=>{
/**
* The tagsList can be tested against an expected snapshot value, as in below.
*/
it ("renders as expected",()=>{
const tree = renderer
.create(<TagsList tags={[`css`,`html`,`typescript`,`coffeescript`]}/>)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
Component Testing
See also React_Testing_Components
Making Components More Testable
From best to worse.
- No internal state - output is a idempotent product of the props
- No side-effects, have these handled by sagas, thunks etc
- No lifecycle hooks - fetching data handled on application level
Component Testing with Redux
React and Redux
These a perceived as a great match because it means
- Components do not generate side effects, only actions
- Components consist of display and container components
- No state in Components
So the approach to testing for React Redux Components is
- Test Container and Display Separately
- Use Unit test to verify methods and props on container
- Use snapshot tests for the display
Redux and Testing mapStateToProps
mapStateToProps is the function responsible for mapping state to the props. Our focus on testing here is only the mapping and not the content. i.e. that the state is appropriately mapped to the props.
import { mapStateToProps } from '../QuestionDetail';
it("should map the state to props correctly", () => {
// Sample App State with sampleQuestion being object in state
const sampleQuestion {
question_id:42,
body: "Space is big"
};
const appState = {
questions: [sampleQuestion]
};
// Sample Props
const ownProps = {
question:42
});
const componentState = mapStateToProps(appState, ownProps)
expect(componentState).toEqual(sampleQuestion);
}
Component Testing with State
Bit of a lot of code but here goes
Create component
import React from 'react';
export default class extends React.Component {
constructor)(...args) {
super(...args);
this.state = {
count: -1
}
}
async comonentDidMount () {
let { count } = await NotificationService.GetNotification();
this.setState( {
count
});
}
render() {
return (
<section className="mt-3 mb-2">
<div className="notifications">
{this.state.count != -1
? '${this.state.count} Notifications Awaiting!'
: 'Loading...'}
</div>
</section>
)
}
}
Create a Service
export default {
async GetNoitifications() {
console.warning("REAL NOTIFICATION SERVICE!");
await delay(1000);
return { count: 42 };
}
}
Test Case
And here is the test case
import NotificationsViewer from '../NotificationsViewer';
import renderer from 'react-test-renderer';
import React from 'react';
import delay from 'redux-saga';
// NOTE Local module you need to pass in url of notification service
jest.mock('../../services/NotificationService');
// NOTE 2 this needs to be a require and needs to be after the jest. Making it
// an import will fail
const notificationService = require('../../services/NotificationService').default;
describe("The notification viewer", () => {
beforeAll( ()=> {
notificationService.__setCount(5);
});
it("should display the correct number of notifications", async()=> {
const tree = renderer
.create(
<NoticationsViewer/>
)
await delay();
const instance = tree.root;
// find by className the component
const component = instance.findByProps( { className: 'notificiations' });
const text = component.children[0];
export(text).toEqual("5 Notifications");
});
});
Mocked Service
Let's make a mocked service in the appropriate place next to the real service. src\services\__mocks__\NotificationService.js
let count = 0;
export default {
__setCount(__count) {
count = _count;
},
async GetNotifications() {
console.warn("Good Job! Using Mock");
}
}
Matchers in Jest
These can be found [1]
Some of them for quick reference.
expect(value) expect.extend(matchers) expect.anything() expect.any(constructor) expect.arrayContaining(array) expect.assertions(number) expect.hasAssertions() expect.not.arrayContaining(array) expect.not.objectContaining(object) expect.not.stringContaining(string) expect.not.stringMatching(string | regexp) expect.objectContaining(object) expect.stringContaining(string) expect.stringMatching(string | regexp) expect.addSnapshotSerializer(serializer)
Transpiling in Jest 2024
I am certainly not expert but here is what I know. There several types of formats for modules,
- commonJS (uses require)
- esm (uses import)
Introduction to issue
When you run jest you may sometimes see
This is because the module, in this case MsalReactTester, cannot be loaded because it is an esm module and you are running jest with commonJS.
Solution
To solve this problem we need to transpile (convert) the module from esm to commonJS. To do this we need to config jest like this.
export default async (): Promise<Config> => {
return {
rootDir: '../',
verbose: true,
setupFilesAfterEnv: ['<rootDir>/configs/setupTests.ts'],
testEnvironment: '<rootDir>/configs/jest.environment.ts',
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': ['babel-jest', { configFile: './configs/babel.config.js' }],
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/configs/__mocks__/fileMock.js',
'^.+\\.(css|sass|scss)$': '<rootDir>/configs/__mocks__/styleMock.js',
},
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
transformIgnorePatterns: ['/node_modules/(?!(uint8array-extras|msal-react-tester)/)'],
...
}
The important lines is the transformIgnorePatterns which tells jest to ignore all modules except uint8array-extras or msal-react-tester. I am hopeless at regex so went there to prove it. You can see only the last two are selected and therefore ignored.
Other Configs
There are other configurations involved too and are provided here for completeness
- setupTests.ts
- jest.environment.ts
// setupTests.ts
import '@testing-library/jest-dom'
import crypto from 'crypto'
Object.defineProperty(window, 'crypto', {
value: {
randomUUID: () => crypto.randomUUID(),
},
})
// jest.environment.ts
import Environment from 'jest-environment-jsdom'
export default class CustomTestEnvironment extends Environment {
async setup() {
await super.setup()
this.global.TextEncoder = TextEncoder
this.global.TextDecoder = TextDecoder
this.global.Response = Response
this.global.Request = Request
}
}