在《使用Webpack等搭建一个适用于React项目的脚手架(2 - React Router、Redux、Sass)》中记录了在项目中使用React-Router、Redux和Sass,以及引入图片。这篇文章主要记录在项目中使用Eslint检查代码风格以及使用Jest等进行单元测试。
使用Eslint
Eslint是用来让代码风格保持一致以及减少错误发生的。
Eslint官网推荐React检查使用eslint-plugin-react。
从这篇官方文档中得知,需要使用@typescript-eslint/parser进行TypeScript检查。在@typescript-eslint/parser的Github仓库中找到检查TypeScript的方法。
本文按照上面提到的这些参考路径进行Eslint配置。(其实在安装好eslint之后,可以用npx eslint --init
创建一个配置。执行npx eslint --init
后会有一些提问,Eslint会根据回答创建配置文件并安装依赖。)
首先安装依赖:
npm i --save-dev eslint eslint-loader eslint-plugin-react
npm i --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
创建配置文件:touch .eslintrc.js
。.eslintrc.js文件内容如下:
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
],
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint/eslint-plugin',
],
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-use-before-define': 'off'
},
}
],
settings: {
react: {
version: 'detect',
}
},
rules: {
'react/jsx-no-undef': 'off'
},
env: {
browser: true,
es2020: true,
node: true,
commonjs: true,
},
globals: {
React: true,
ReduxConnect: true,
Axios: true,
UseEffect: true,
},
ignorePatterns: ['dist/'],
}
eslint:recommended
表示启用Eslint推荐规则,plugin:react/recommended
启用React推荐规则。
在overrides中进行配置,只对.ts文件和.tsx文件进行TypeScript相关的处理,比如只对.ts、.tsx文件使用@typescript-eslint/parser
处理器。
设置react: { version: 'detect' }
表示自动设置检查的React版本是安装的React的版本。
在env中的配置表示支持browser、es2020、node、commonjs中预定义的全局变量。
在globals中配置之前的步骤中设置的全局变量,比如React。
ignorePatterns配置忽略的文件,数组的每一个元素相当于.eslintignore中的一行,.eslintignore能覆盖ignorePatterns的配置。Eslint默认会忽略/node_modules/* 和 /bower_components/*文件目录,所以没在ignorePatterns中添加这两个目录。
根据需要关掉了几条规则:
1.关掉在React中不允许未声明变量的规则,因为我们全局设置了React变量,但是Eslint会报错error 'React' is not defined react/jsx-no-undef
,所以把这个规则关掉。
'react/jsx-no-undef': 'off'
2.关掉不能在声明前使用的规则,还是因为已经在Webpack中定义了全局变量React,但是Eslint无法进行判断,所以会报错'React' was used before it was defined
。
'@typescript-eslint/no-use-before-define': 'off'
3.关掉不能使用// @ts-ignore
的规则:
'@typescript-eslint/ban-ts-ignore': 'off',
4.关掉不能使用any定义类型的规则(当然尽量不要使用any定义类型):
'@typescript-eslint/no-explicit-any': 'off',
在package.json中添加:
"scripts": {
...
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
执行npm run lint
,会看见检查的结果,根据提示调整代码。
在Webpack(config/webpack.dev.js)中配置如下,表示在对.js、.jsx、.ts、.tsx文件进行处理前先使用eslint-loader对文件进行Eslint检查。
{
enforce: 'pre',
test: /(\.js(x?))|(\.ts(x?))$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'eslint-loader',
},
执行npm start
启动webpack-dev-server,当文件修改并保存之后会重新打包,在打包前会进行Eslint检查,可以根据检查结果进行代码调整。
使用Jest
Jest是一个专注于简化的JavaScript测试框架。本文参考Jest官方文档--使用React、react-testing-library--基础例子进行了简单的DOM测试。React Redux测试和React Router测试等可以查看react-testing-library--更多例子。
安装依赖:
npm i --save-dev jest @testing-library/react
根据Jest官方文档--使用Babel的内容,安装Jest的时候自动安装了babel-jest
,babel-jest
会根据babel配置自动转换文件,(《使用Webpack等搭建一个适用于React项目的脚手架(1 - React、TypeScript)》中已经在.babelrc.js中进行了babel配置,所以这里不用再考虑如何处理tsx文件)。
在项目根目录创建一个__tests__
文件夹。__tests__
文件夹下包含测试文件,创建了3个文件夹分别表示测试组件、测试页面、测试函数。
├── __tests__
│ ├── components
│ ├── functions
│ └── pages
│ └── Novel.test.tsx
如果要对__tests__
文件夹中的内容进行类型检查,需要安装jest类型定义包,并且在tsconfig.json文件夹中引入__tests__
文件夹。
npm i --save-dev @types/jest
在tsconfig.json的include中添加上__tests__
目录:
...
"include": [
...
"__tests__/*",
"__tests__/**/*",
]
}
1.写一个例子
调整src/pages/Novel/Novel.tsx文件内容如下:
export default function Novel(): JSX.Element {
const [ novels, setNovels ] = UseState(['一号小说', '二号小说']);
function deleteNovel (index): void {
if (!novels[index]) return;
const nextNovels = [...novels];
nextNovels.splice(index, 1);
setNovels(nextNovels);
}
return (
<div>
<ul>
{novels.map((novel, index) => {
return <li key={index} onClick={deleteNovel.bind(this, index)} style={{ cursor: 'pointer' }}>{novel}</li>;
})}
</ul>
</div>
);
}
组件渲染小说列表,点击某选项就删除该选项。比如点击渲染出的<li>一号小说</li>
,<li>一号小说</li>
就会被删除。
tests/pages/Novel.test.tsx文件内容如下:
import { render, fireEvent, screen } from '@testing-library/react';
import Novel from '@/pages/Novel/Novel.tsx';
test('novel item should not exist after deletion', () => {
const novelName = '一号小说';
render(<Novel />);
expect(screen.queryByText(novelName)).toBeTruthy();
fireEvent.click(screen.queryByText(novelName));
expect(screen.queryByText(novelName)).toBeNull();
});
这个测试文件简单地测试以下内容:
组件渲染成DOM后,textContent为'一号小说'的元素(A元素)是存在的,点击A元素之后,DOM中就没有A元素了。
关于代码中用到的test、fireEvent等更详细的信息可以查看Jest官网--使用matchers、testing-library官网、firEvent模拟事件。
2.Jest配置
在前几篇文章的代码中,我们已经设置了React等全局变量,以及使用@作为src目录的路径别名。Jest中也需要做相应的配置,否则执行npx jest
会报错。
在根目录下创建一个jest.config.js文件,用于进行jest配置。
touch jest.config.js
jest.config.js文件内容如下:
module.exports = {
setupFiles: ['<rootDir>/__tests__/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testPathIgnorePatterns: ['<rootDir>/__tests__/jest.setup.js', '/node_modules/']
}
(1)配置全局变量
jest配置中有一个globals属性用于配置全局变量,但是globals对象设置的对象必须是JSON可序列化的,而函数在使用JSON.stringify()转换的时候会被忽略,所以需要使用setupFiles属性。<rootDir>
是jest配置文件所在的文件目录。
jest.setup.js文件内容如下:
import React, { useState } from 'react';
global['React'] = React;
global['UseState'] = useState;
因为这里要测试的组件只用到的React和UseState,所以只配置了React和useState,需要用到别的全局变量时,可以按照相同的方法配置。
(2)配置路径别名
在moduleNameMapper属性中设置'^@/(.*)$': '<rootDir>/src/$1'
用于配置路径别名,就像之前我们在Webpack和TypeScript中配置的那样,用@代表src目录。一开始我使用的是'^@(.*)$': '<rootDir>/src$1'
,这样执行npx jest
的时候会报错:
● Test suite failed to run
Configuration error:
Could not locate module @babel/code-frame mapped as:
.../simple-scaffold02/src$1.
经过反复横跳检查,两边的正则表达式都是没错的,但是仔细观察报错信息,发现这样配置把@babel/code-frame引用也映射到src路径下了,然而src路径下是没有babel/code-frame的,自然报错了。所以需要以加一根斜杠的方式匹配路径'^@/(.*)$': '<rootDir>/src/$1'
。
(3)设置忽略文件
Jest默认会将匹配正则表达式(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$
的文件作为测试文件,前面的jest.setup.js文件是放在__tests__
文件夹中的,但它是用来设置全局变量的,并不是测试文件。所以需要在testPathIgnorePatterns中配置忽略它。
(4)Webpack和TypeScript新增配置。
因为新增了一个全局变量UseState代表React的useState模块,所以在Webpack和TypeScript中也需要加上这个全局变量:
config/webpack.common.js:
new webpack.ProvidePlugin({
...
UseState: ['react', 'useState'],
}),
typings/react.d.ts:
import React, { useEffect, useState } from 'react';
declare global {
...
const UseState: typeof useState;
}
(5)修改package.json:
"scripts": {
...
"test": "jest"
},
执行npm run test
执行测试用例,得到结果:
> jest
PASS __tests__/pages/Novel.test.tsx
✓ novel item should not exist after deletion (33ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.241s
(6)检查代码风格是否统一
执行npm run lint
进行检查,发现因为新创建的jest.setup.js文件中的import React, { useState } from 'react';
造成了一个报错:
error Parsing error: 'import' and 'export' may appear only with 'sourceType: module
只有配置了sourceType: module
才能使用import和export。之前的代码中也用了import,但因为是在ts文件和tsx文件中,所以eslint没有报这个错。
在**.eslintrc.js**中添加上以下内容即可:
module.exports = {
...
parserOptions: {
sourceType: 'module',
},
}
其他
1.根据需要忽略不想使用git上传的文件
touch .gitignore
文件内容如下:
.DS_Store
.vscode
dist
node_modules
比较全的gitignore配置可以参考gitignore例子合集。
2.创建README.md
# npm start
start up project.
# npm run build
build for production environment.
# npm run buid:dev
build for development environment.
# npm run lint
use elint to find problems in code.
# npm run test
user jest to test code.