[Testing] Config jest to test Javascript Application -- Part 2
Setup an afterEach Test Hook for all tests with Jest setupTestFrameworkScriptFile
With our current test situation, we have a commonality between most of our tests. In each one of them, we're importing 'react-testing-library/cleanup-after-each' in our __tests__/calculator.js
, and __tests__/auto-scaling-text.js
, and __tests__/calculator-display.js
.
__tests__/auto-scaling-text.js
1 import 'react-testing-library/cleanup-after-each' 2 import React from 'react' 3 import {render} from 'react-testing-library' 4 import AutoScalingText from '../auto-scaling-text' 5 6 test('renders', () => { 7 const {container} = render(<AutoScalingText />) 8 console.log(container.innerHTML) 9 })
__tests__/calculator.js
import 'react-testing-library/cleanup-after-each' import React from 'react' import {render} from 'react-testing-library' import Calculator from '../../calculator' test('renders', () => { render(<Calculator />) })
__tests__/calculator-display.js
.
import 'react-testing-library/cleanup-after-each' import React from 'react' import {render} from 'react-testing-library' import CalculatorDisplay from '../calculator-display' import {createSerializer} from 'jest-emotion'; import * as emotion from 'emotion'; expect.addSnapshotSerializer(createSerializer(emotion)); test('mounts', () => { const {container} = render(<CalculatorDisplay value="0" />) expect(container.firstChild).toMatchSnapshot() })
There are some common code we use in all the tests files:
import 'react-testing-library/cleanup-after-each'
And also, emotion libaray will be used a lot in the future project, therefore, we want those code can be automaticlly import into each test file to reduce code duplication.
Create a test/setup-tests.js:
import 'react-testing-library/cleanup-after-each' import {createSerializer} from 'jest-emotion'; import * as emotion from 'emotion'; expect.addSnapshotSerializer(createSerializer(emotion));
Then in the jest.config.js file, we config jest to import a file before running each test:
module.exports = { testEnvironment: 'jest-environment-jsdom', //'jest-environment-node', moduleNameMapper: { '\\.module\\.css$': 'identity-obj-proxy', '\\.css$': require.resolve('./test/style-mock.js') }, snapshotSerializers: ['jest-serializer-path'], // after jest is loaded setupTestFrameworkScriptFile: require.resolve('./test/setup-tests.js') }
Support custom module resolution with Jest moduleDirectories
Webpack’s resolve.modules
configuration is a great way to make common application utilities easily accessible throughout your application. We can emulate this same behavior in Jest using the moduleDirectories
configuration option.
For the calculator.js, it is using dynamic loading with webpack:
import React from 'react' import loadable from 'react-loadable'' const CalculatorDisplay = loadable({ loader: () => import('calculator-display').then(mod => mod.default), loading: () => <div style={{height: 120}}>Loading display...</div>, })
In our test:
import React from 'react' import {render} from 'react-testing-library' import Calculator from '../../calculator' test('renders', () => { const {container, debug} = render(<Calculator />); debug(container); })
It logs out:
<div class="calculator" > <div style="height: 120px;" > Loading display... </div> <div class="calculatorKeypad" > <div class="inputKeys" > ...
We can see it is not fully loaded yet.
We can solve the problem:
import React from 'react' import {render} from 'react-testing-library' import loadable from 'react-loadable' import Calculator from '../calculator' test('renders', async () => { await loadable.preloadAll(); const {container, debug} = render(<Calculator />); debug(container); })
It show the error:
UnhandledPromiseRejectionWarning: Error: Cannot find module 'calculator-display' from 'calculator.js' at Resolver.resolveModule
The problem is because in the Calculator.js we import component as if there were a node module:
const CalculatorDisplay = loadable({ loader: () => import('calculator-display').then(mod => mod.default), loading: () => <div style={{height: 120}}>Loading display...</div>, })
but it's not a node module. It actually lives in the shared
directory as calculator-display
. The way that it works in the app is we have our webpack configuration set to resolve
modules
to node_modules
, just like node would in a regular commonJS environment.
resolve: { modules: ['node_modules', path.join(__dirname, 'src'), 'shared'] }
Any of these files inside of our src
directory, if they're inside of a shared, they can actually be treated as if they were in node modules, which is a really handy thing for a giant project.
However, that poses a problem for us in Jest because Jest doesn't consume this webpack configuration. It doesn't resolve the modules the way webpack is resolving them.
The way to solve the project is by add those webpack resolve config into jest config as well:
const path = require('path'); module.exports = { testEnvironment: 'jest-environment-jsdom', //'jest-environment-node', moduleDirectories: ['node_modules', path.join(__dirname, 'src'), 'shared'], moduleNameMapper: { '\\.module\\.css$': 'identity-obj-proxy', '\\.css$': require.resolve('./test/style-mock.js') }, snapshotSerializers: ['jest-serializer-path'], // after jest is loaded setupTestFrameworkScriptFile: require.resolve('./test/setup-tests.js') }
the same in the webpack.config.js:
const path = require('path') module.exports = { entry: './src/index.js', output: { path: path.resolve('dist'), filename: 'bundle.js', }, resolve: { modules: ['node_modules', path.join(__dirname, 'src'), 'shared'], }, module: {
Support a test utilities file with Jest moduleDirectories
Every large testbase has common utilities that make testing easier to accomplish. Whether that be a custom render function or a way to generate random data. Let’s see how we can make accessing these custom utilities throughout the tests easier using Jest’s moduleDirectories
feature.
Sometime we are using different kinds of Providers:
import React from 'react' import {ThemeProvider} from 'emotion-theming' import Calculator from './calculator' import * as themes from './themes' class App extends React.Component { state = {theme: 'dark'} handleThemeChange = ({target: {value}}) => this.setState({theme: value}) render() { return ( <ThemeProvider theme={themes[this.state.theme]}> <React.Fragment> <Calculator />
For that we have to update our tests components to adopt the changes.
import React from 'react' import {render} from 'react-testing-library' import CalculatorDisplay from '../shared/calculator-display' import {ThemeProvider} from 'emotion-theming' import {dark} from '../themes' function renderWithProvider (ui, options) { return render( <ThemeProvider theme={dark}> {ui} </ThemeProvider>, options ); } test('mounts', () => { const {container} = renderWithProvider(<CalculatorDisplay value="0" />) expect(container.firstChild).toMatchSnapshot() })
One thing we want to do to simplfiy the process is by creating a 'render' function with render the component with all the Providers which is necessary, therefore I don't need to worry about wirte Provider wrap every times inside the tests.
So we create a new file in test/calculator-test-util.js:
import React from 'react' import {render} from 'react-testing-library' import {ThemeProvider} from 'emotion-theming' import {dark} from '../src/themes' function renderWithProviders (ui, options) { return render( <ThemeProvider theme={dark}> {ui} </ThemeProvider>, options ); } export * from 'react-testing-library'; export {renderWithProviders as render};
test:
import React from 'react' import {render} from '../../test/calculator-test-util' import CalculatorDisplay from '../shared/calculator-display' test('mounts', () => { const {container} = render(<CalculatorDisplay value="0" />) expect(container.firstChild).toMatchSnapshot() })
Now the 'calculator-test-util.js' can be used in multi files as well, so one thing we want further imporve is:
import {render} from '../../test/calculator-test-util'
As the project grows, the nested path will go crazy, so the way we want is:
import {render} from '.calculator-test-util'
To achieve that, we need to modify the jest.config.js:
const path = require('path'); module.exports = { testEnvironment: 'jest-environment-jsdom', //'jest-environment-node', moduleDirectories: [ 'node_modules', path.join(__dirname, 'src'), 'shared', path.join(__dirname, 'test'), ], moduleNameMapper: { '\\.module\\.css$': 'identity-obj-proxy', '\\.css$': require.resolve('./test/style-mock.js') }, snapshotSerializers: ['jest-serializer-path'], // after jest is loaded setupTestFrameworkScriptFile: require.resolve('./test/setup-tests.js') }
Because in calculator-test-utils.js we export everything from 'react-testing-library', so we can replace everywhere we use it.
last thing is eslint shows the error:
[eslint] Unable to resolve path to module 'calculator-test-util'. [import/no-unresolved]
Install:
npm i -D eslint-import-resolver-jest
Adjust the eslint config:
module.exports = { extends: [ 'kentcdodds', 'kentcdodds/import', 'kentcdodds/webpack', 'kentcdodds/jest', 'kentcdodds/react', ], overrides: [ { files: ['**/__tests__/**'], settings: { 'import/resolver': { jest: { jestConfigFile: path.join(__dirname, './jest.config.js'), } } } } ] }
Now eslint can help with checking our package name is correct.
Step through Code in Jest using the Node.js Debugger and Chrome DevTools
Sometimes it can be a real challenge to determine what’s going on when testing your code. It can be really helpful to step through your code in a debugger. In this lesson we’ll see how to use Jest’s --runInBand flag with node’s --inspect-brk to debug our tests in Chrome’s debugger.
--runBand: make jest run in sequence
--inspect-brk: enable debugger in node for jest
Create script in package.json:
"test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand",
Run:
npm run test:debug
Open:
chrome://inspect/#devices
Then you can get broswer debugging experience.
Configure Jest to report code coverage on project files
Jest comes with code coverage reporting built-into the framework, let’s see how quick and easy it is to add code coverage reporting to our project and take a look at the generated report.
There maybe some folder we don't want to include into our test coverage to get a more actual coverage report. To do that in jest.config.js:
const path = require('path'); module.exports = { testEnvironment: 'jest-environment-jsdom', //'jest-environment-node', moduleDirectories: [ 'node_modules', path.join(__dirname, 'src'), 'shared', path.join(__dirname, 'test'), ], moduleNameMapper: { '\\.module\\.css$': 'identity-obj-proxy', '\\.css$': require.resolve('./test/style-mock.js') }, snapshotSerializers: ['jest-serializer-path'], // after jest is loaded setupTestFrameworkScriptFile: require.resolve('./test/setup-tests.js'), collectCoverageFrom: ['**/src/**/*.js'], }
Set a code coverage threshold in Jest to maintain code coverage levels
Wherever you are at with code coverage you generally don’t want that level to go down. Let’s add coverage thresholds globally as well as in specific files to ensure we never drop below a certain level of coverage.
const path = require('path'); module.exports = { testEnvironment: 'jest-environment-jsdom', //'jest-environment-node', moduleDirectories: [ 'node_modules', path.join(__dirname, 'src'), 'shared', path.join(__dirname, 'test'), ], moduleNameMapper: { '\\.module\\.css$': 'identity-obj-proxy', '\\.css$': require.resolve('./test/style-mock.js') }, snapshotSerializers: ['jest-serializer-path'], // after jest is loaded setupTestFrameworkScriptFile: require.resolve('./test/setup-tests.js'), collectCoverageFrom: ['**/src/**/*.js'], coverageThreshold: { global: { statements: 80, branchs: 80, lines: 80, functions: 80, }, // for single file coverage threshold './src/shared/utils.js': { statements: 100, branchs: 80, lines: 100, functions: 100, } } }
Report Jest Test Coverage to Codecov through TavisCI
The coverage report generated by Jest is fantastic, but it’d be great to track that coverage over time and be able to review that coverage at a glance, maybe even put it up on a display in the office! Codecov.io is a fantastic service that can consume our code coverage report and it integrates great with GitHub. Let’s see how we can extend our existing Travis build configuration to send the coverage report to Codecov.io.
// .travis.yml sudo: false language: node_js cache: directories: - ~/.npm notifications: email: false node_js: '8' install: npm install script: npm run validate after_script: npx codecov@3 branches: only: master
相关文章
- JavaScript小技能:事件
- [译] JavaScript -- Map vs ForEach
- 用javascript分类刷leetcode3.动态规划(图文视频讲解)
- JSON显示库 -- showJson (Javascript)
- 杨校老师课堂之JavaScript定时器案例的红绿灯设计--原始写法
- JavaScript学习总结(十六)——Javascript闭包(Closure)详解编程语言
- Javascript实例教程(19)使用HoTMetal(7)
- 无语,javascript居然支持中文(unicode)编程!
- Javascript常用运算符(Operators)-javascript基础教程
- 走出JavaScript初学困境—js初学
- JavaScript验证浏览器是否支持javascript的方法小结
- javascript设置文本框中焦点的位置
- JavaScript学习笔记(十一)
- JavaScript设计模式富有表现力的Javascript(一)
- 动态载入/删除/更新外部JavaScript/Css文件的代码
- JavaScript中值类型与引用类型实例说明
- 你必须知道的Javascript知识点之"单线程事件驱动"的使用
- 深入Javascript函数、递归与闭包(执行环境、变量对象与作用域链)使用详解
- javascript数字时钟示例分享
- 一些老手都不一定知道的JavaScript技巧
- javascript学习笔记--数字格式类型
- JavaScript中使用document.write向页面输出内容实例
- 原生JavaScript+LESS实现瀑布流
- Javascript中this的用法详解