关于jest:Jest-React-Native-Mock-AppState-TypeError

问题记录背景:通过 Jest 和 React Testing Library 对 React Native 做自动化测试问题: 代码中呈现 AppState 的应用 import { AppState } from 'react-native';AppState.removeEventListener('change', handleAppStateChange);报错:TypeError: import_react_native.AppState.removeEventListener is not a function jest.config.js 配置应用了 React Native 预设 module.exports = { preset: 'react-native', ...通过排查发现是因为 React Native 提供的 jest mock 文件缺失局部办法 门路:*/react-native/jest/setup.js .mock('react-native/Libraries/AppState/AppState', () => ({ addEventListener: jest.fn(() => ({ remove: jest.fn(), })), }))解决方案:我的项目长期计划是将 packages/react-native/jest/setup.js 拷贝下来,减少 react-native/Libraries/AppState/AppState 模块的 removeEventListener 和 currentState办法的mock .mock('react-native/Libraries/AppState/AppState', () => ({ addEventListener: jest.fn(() => ({ remove: jest.fn(), })), removeEventListener: jest.fn(), currentState: jest.fn(), }))而后放在我的项目仓库中在 jest.setup.js 文件头部引入 ...

September 25, 2023 · 1 min · jiezi

关于jest:jest-ts-esm

Jest 是当下最支流的前端测试框架首先初始化ts环境 yarn add typescript --devnpx tsc --init第二步:装置ts下的jestyarn add jest @types/jest --dev 第三步:新建tests文件夹tests/index.spec.ts it('init',()=>{ expect(true).toBe(true)})此处记住要在tsconfig.json外面批改配置 "types": ["jest"], 第四步package.json增加 "scripts": { "test":"jest" },执行npm run test失去 接下来解决导入模块的问题index.ts export function add (a,b){ return a+b}index.spec.ts import { add } from "../index"it('init',()=>{ expect(add(2,3)).toBe(5)})执行npm run test报错 关上https://www.jestjs.cn/docs/getting-started配置 Babel npm install --save-dev babel-jest @babel/core @babel/preset-env在我的项目的根目录下创立 babel.config.js ,通过配置 Babel 使其可能兼容以后的 Node 版本。 babel.config.jsmodule.exports = { presets: [['@babel/preset-env', {targets: {node: 'current'}}]],};应用 Typescript通过 babel 来反对 Typescript通过 Babel,Jest 可能反对 Typescript。首先要确保你遵循了上述 应用 Babel 指引。接下来装置 @babel/preset-typescript 插件: ...

April 11, 2023 · 1 min · jiezi

关于jest:初学jest如何配置支持esmodulets

根底应用装置jestyarn add jest -D配置package.json{ "scripts": { "test": "jest" }, "devDependencies": { "jest": "^27.5.1" }}测试代码// sum.jsmodule.exports = function sum(a, b) { return a + b;};// sum.spec.jsconst sum = require("./sum");test("sum", () => { expect(sum(1, 1)).toBe(2);});测试yarn test没有问题 配置反对esmodule未做任何配置,间接将导入导出改为esmodule将会呈现这样的谬误 官网文档 只须要在package.json中一点配置即可反对esmodule { "license": "ISC", "type": "module", "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest" }, "devDependencies": { "jest": "^27.5.1" }}容许测试胜利,不过会有一个提醒说VM Modules是一个试验个性 配置反对ts除了jest须要装置@types/jest ts-jest typescript这三个包 yarn add ts-jest @types/jest typescript -D配置文件jest.config.jsmodule.exports = { preset: "ts-jest", testEnvironment: "node",};配置tsconfig.json没有esModuleInterop属性会又一些提醒,也能跑,package外面失常写"test": "jest"就行 ...

March 15, 2022 · 1 min · jiezi

关于jest:Jest-如何将复杂的判断条件中的具体问题暴露出来

在写测试的时候,如果你须要对大量的数据进行 compare 解决的时候,你大概率不会把所有须要比照的对象都列出来,而是抉择间接循环解决。 在测试中如果有循环解决的时候,很有可能会呈现的一个问题是你可能无奈在测试无奈通过时疾速定位道具体是循环中的哪一个元素呈现的问题。这个时候的定位就会比拟麻烦。 一个比拟好的方法是,能够在 Jest 中退出 try/catch 中来处理错误,这样能够在呈现谬误的时候,打印一些辅助信息来疾速定位,比方 it('test-error-catch-example',() => { let needTestData = [1,2,3,4] needTestData.foreach( item => { let result = doSomething(item) // 这里开始是新增的 try{ expect(result).toBe(true) }catch(e){ console.log("error key",item) throw e; } // 新增的错误处理完结 })})通过增加一个自定义的 try catch ,能够在呈现问题的时候,一方面将 Error 依照惯例的形式抛出,期待 Jest 解决,另一方面,能够在 catch 时输入自定义的信息,不便咱们进行排查和修复。

February 18, 2022 · 1 min · jiezi

关于jest:Jest-测试框架-expect-和-匹配器-matcher-的设计原理解析

副标题:SAP Spartacus SSR 优化的单元测试剖析之二 - 调用参数检测源代码: it(`should pass parameters to the original engine instance`, () => { expect(originalEngineInstance).toHaveBeenCalledWith( mockPath, mockOptions, mockCallback ); }); 留神察看 jest.Expect 的返回值:类型为 jest.JestMatchersShape 单步调试 expect 的调用过程: 从正文看,该函数为 spec 创立一个 expectation, 传入的 actual 为 spy 之后的版本。 结构一个 expectation,须要以上的参数。 紧接着调用 toHaveBeenCalledWith: 反对的所有办法,在 Chrome 开发者工具里可能看到: toHaveBeenCalledWith 执行到这里来了: 所有可用的匹配器 matchers,都定义在文件 jasmine.js 里: 因而,toHaveBeenCalledWith 也算匹配器 matchers 之一。 结构一个匹配器实例: 调用匹配器工厂,结构一个匹配器实例: 其实例的运行代码如下图所示: ...

October 2, 2021 · 1 min · jiezi

关于jest:Jest-测试框架-beforeEach-的设计原理解析

副标题:SAP Spartacus SSR 优化的单元测试剖析之一 : beforeEach 文档 SAP Spartacus 里这段代码: originalEngine = jasmine .createSpy('ngExpressEngine') .and.callFake(() => originalEngineInstance);该办法承受一个字符串作为创立的 Spy 的名称,返回一个新的 Spy 对象。 这个新创建的 spy 对象,还是位于 jasmine namespace 之下。 spy.and: 返回 SpyStrategy 实例: 接下来,咱们就能够通过这个 spy 对象的 strategy 办法,指派这个 spy 去做一些事件了。 callFake:callFake(fn) Tell the spy to call a fake implementation when invoked.单步调试 createSpy 办法: 转交给 env: 在 jasmine 外部,新建 strategy dispatcher 和 callTracker: wrapper 的 and 属性,来自 strategy dispatcher 的 and 属性: ...

October 2, 2021 · 1 min · jiezi

关于jest:Jest-测试框架使用的学习笔记

Jest Tutorial for Beginners: Getting Started With JavaScript Testing Jest 是一个 JavaScript 测试运行器,即用于创立、运行和构建测试的 JavaScript 库。 Jest 作为 NPM 包公布,您能够将其装置在任何 JavaScript 我的项目中。 Jest 是当今最风行的测试运行器之一,也是 React 我的项目的默认抉择。 Setting up the project与每个 JavaScript 我的项目一样,您须要一个 NPM 环境(确保在您的零碎上装置了 Node)。 创立一个新文件夹并应用以下命令初始化我的项目: mkdir getting-started-with-jest && cd $_npm init -y接着: npm i jest --save-dev让咱们也配置一个 NPM 脚本来从命令行运行咱们的测试。 关上 package.json 并配置一个名为 test 的脚本来运行 Jest: "scripts": { "test": "jest" },Specifications and test-driven development作为开发人员,咱们都喜爱创意自在。 然而,在大多数状况下,当波及到庄重的事件时,您没有那么多特权。 咱们必须遵循标准,即对要构建的内容的书面或口头形容。 在本教程中,咱们从项目经理那里失去了一个相当简略的标准。 一个超级重要的客户端须要一个 JavaScript 函数来过滤一个对象数组。 ...

October 2, 2021 · 2 min · jiezi

关于jest:jest钩子函数执行顺序

其实就是一个同步代码执行的程序顺便触发了对应的钩子函数; beforeAll(() => console.log('1 - beforeAll'));afterAll(() => console.log('1 - afterAll'));beforeEach(() => console.log('1 - beforeEach'));afterEach(() => console.log('1 - afterEach'));test('', () => console.log('1 - test'));describe('Scoped / Nested block', () => { beforeAll(() => console.log('2 - beforeAll')); afterAll(() => console.log('2 - afterAll')); beforeEach(() => console.log('2 - beforeEach')); afterEach(() => console.log('2 - afterEach')); test('', () => console.log('2 - test'));});// 1 - beforeAll// 1 - beforeEach// 1 - test// 1 - afterEach// 2 - beforeAll// 1 - beforeEach// 2 - beforeEach// 2 - test// 2 - afterEach// 1 - afterEach// 2 - afterAll// 1 - afterAll

September 20, 2021 · 1 min · jiezi

关于jest:GrowingIO-Design-组件库搭建之单元测试

前言GrowingIO Design 是用 React 编写的组件库,实质上就是 React 组件,你能够用像测试其余 JavaScript 代码相似的形式测试 React 组件。当初有许多种测试 React 组件的办法。大体上能够被分为两类: 渲染组件树:在一个简化的测试环境中渲染组件树并对它们的输入做断言查看。运行残缺利用:在一个实在的浏览器环境中运行整个利用。咱们对于第一类称之为单元测试,本文次要专一于这种状况的测试策略;第二类称为端到端(end-to-end)测试,残缺的端到端测试在避免对重要工作流的屡次回归方面很有价值,对于组件的端到端测试会有专门一篇文章来介绍。 工具调研对于测试工具的抉择,次要从 2020 年的 State of JavaScript Survey 中列出的工具筛选。咱们先从“应用度”和“满意度”两个角度来看测试生态圈的比拟常用工具。 测试工具应用度比照 测试工具满意度比照 Jest、Storybook 在“应用度”和“满意度”上都取得了比拟高的分数,新进入的 Testing Library 在“满意度”上也取得了高分。 注: 满意度:会再次应用 / (会再次应用 + 不会再次应用) 应用度:(将再次应用 + 将不再应用) / 总计除了以上的几款工具,看看其余工具的用户百分比: 工具抉择先看看 React 官方网站(目前最新版本 v17.0.2)举荐的工具: Jest 是一个 JavaScript 测试运行器。它容许你应用 jsdom 操作 DOM 。只管 jsdom 只是对浏览器工作体现的一个近似模仿,对测试 React 组件来说它通常也曾经够用了。Jest 有着非常优良的迭代速度,同时还提供了若干弱小的性能,比方它能够模仿 modules 和 timers 让你更精密的控制代码如何执行。React Testing Library(简称:RTL)是一组能让你不依赖 React 组件具体实现对他们进行测试的辅助工具。它让重构工作变得轻而易举,还会推动你拥抱无关无障碍的最佳实际。尽管它不能让你省略子元素来浅(shallowly)渲染一个组件,但像 Jest 这样的测试运行器能够通过 mocking 让你做到。实际上在 v16.7.0 版本的网站上,他们还举荐了另一个工具: ...

August 19, 2021 · 3 min · jiezi

前端自动化测试二

上一篇文章,我们已经讲述了Jest中的基本使用,这一篇我们来说说如何深度使用Jest 在测试中我们会遇到很多问题,像如何测试异步逻辑,如何mock接口数据等... 通过这一篇文章,可以让你在开发中对Jest的应用游刃有余,让我们逐一击破各个疑惑吧! 1.Jest进阶使用1.1 异步函数的测试提到异步无非就两种情况,一种是回调函数的方式,另一种就是现在流行的promise方式 export const getDataThroughCallback = fn => { setTimeout(() => { fn({ name: "webyouxuan" }); }, 1000);};export const getDataThroughPromise = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ name: "webyouxuan" }); }, 1000); });};我们来编写async.test.js方法 import {getDataThroughCallback,getDataThroughPromise} from './3.getData';// 默认测试用例不会等待测试完成,所以增加done参数,当完成时调用done函数it('测试传入回调函数 获取异步返回结果',(done)=>{ // 异步测试方法可以通过done getDataThroughCallback((data)=>{ expect(data).toEqual({ name:'webyouxuan' }); done(); })})// 返回一个promise 会等待这个promise执行完成it('测试promise 返回结果 1',()=>{ return getDataThroughPromise().then(data=>{ expect(data).toEqual({ name:'webyouxuan' }); })})// 直接使用async + await语法it('测试promise 返回结果 2',async ()=>{ let data = await getDataThroughPromise(); expect(data).toEqual({ name:'webyouxuan' });})// 使用自带匹配器it('测试promise 返回结果 3',async ()=>{ expect(getDataThroughPromise()).resolves.toMatchObject({ name:'webyouxuan' })})2.Jest中的mock2.1 模拟函数jest.fn()为什么要模拟函数呢?来看下面这种场景,你要如何测试 ...

September 10, 2019 · 3 min · jiezi

前端自动化测试一

目前开发大型应用,测试是一个非常重要的环节,但是大多数前端开发者对测试相关的知识是比较缺乏的。因为可能项目开发周期短根本没有机会写,所以你没有办法体会到前端自动化测试的重要性。 来说说为什么前端自动化测试如此重要! 先看看前端常见的问题: 修改某个模块功能时,其它模块也受影响,很难快速定位bug多人开发代码越来越难以维护不方便迭代,代码无法重构代码质量差增加自动化测试后: 我们为核心功能编写测试后可以保障项目的可靠性强迫开发者编写更容易被测试的代码,提高代码质量编写的测试有文档的作用,方便维护1.测试简介1.1 黑盒测试和白盒测试黑盒测试一般也被称为功能测试,黑盒测试要求测试人员将程序看作一个整体,不考虑其内部结构和特性,只是按照期望验证程序是否能正常工作白盒测试是基于代码本身的测试,一般指对代码逻辑结构的测试。1.2 测试分类单元测试(Unit Testing) 单元测试是指对程序中最小可测试单元进行的测试,例如测试一个函数、一个模块、一个组件... 集成测试(Integration Testing) 将已测试过的单元测试函数进行组合集成暴露出的高层函数或类的封装,对这些函数或类进行的测试 端到端测试(E2E Testing)打开应用程序模拟输入,检查功能以及界面是否正确 1.3 TDD & BDDTDD是测试驱动开发(Test-Driven Development) TDD的原理是在开发功能代码之前,先编写单元测试用例代码 BDD是行为驱动开发(Behavior-Driven Development) 系统业务专家、开发者、测试人员一起合作,分析软件的需求,然后将这些需求写成一个个的故事。开发者负责填充这些故事的内容,保证程序实现效果与用户需求一致。 小结: TDD是先写测试再开发 (一般都是单元测试,白盒测试);而BDD则是按照用户的行为来开发,再根据用户的行为编写测试用例 (一般都是集成测试,黑盒测试)1.4 测试框架Karma:Karma为前端自动化测试提供了跨浏览器测试的能力,可以在浏览器中执行测试用例Mocha:前端自动化测试框架,需要配合其他库一起使用,像chai、sinon...Jest:Jest 是Facebook推出的一款测试框架,集成了 Mocha,chai,jsdom,sinon等功能。...看到这里Facebook 都在推Jest,你还不学吗? Jest也有一些缺陷就是不能像Karma这样直接跑在浏览器上,它采用的是jsdom,优势是简单、0配置! 后续我们通过Jest来聊聊前端自动化测试。 2.Jest的核心应用在说Jest测试之前,先来看看以前我们是怎样测试的 const parser = (str) =>{ const obj = {}; str.replace(/([^&=]*)=([^&=]*)/g,function(){ obj[arguments[1]] = arguments[2]; }); return obj;}const stringify = (obj) =>{ const arr = []; for(let key in obj){ arr.push(`${key}=${obj[key]}`); } return arr.join('&');}// console.log(parser('name=zf')); // {name:'zf'}// console.log(stringify({name:'zf'})) // name=zf我们每写完一个功能,都先需要手动测试功能是否正常,测试后可能会将测试代码注释起来,这样会产生一系列问题。因为会污染源代码,所有的测试代码和源代码混合在一起。如果删除掉,下次测试还需要重新编写。 ...

September 9, 2019 · 2 min · jiezi

前端单元测试入门2jest

1.常用测试框架qunit jQuerymocha 支持Node&Browser express.jsjasmine支持Node&Browser Vue.jskarma A Test-Runner 在不同的浏览器中跑测试用例 Angularjest React 零配置内置代码覆盖率内置Mocks2 jest2.1 安装npm i jest --save-dev npm i jest -g2.2 编写**/test.js 2.3 运行npx run jestnpm run test(配置scripts)2.4 编写测试用例Test Suits 测试套件,每个文件就是一个套件Test Group 分组 describeTest Case 测试用途 test()Assert 断言 expect()//qs.test.jslet { parse, stringify } = require('./qs');describe('parse', () => {//分组 test('one', () => {//测试用例 expect(parse("name=zfpx").name).toBe('zfpx'); }); test('two', () => { expect(parse("name=zfpx&age=9").age).toBe('9'); });});describe('stringify', () => { test('one', () => { expect(stringify({ name: 'zfpx' })).toBe('name=zfpx'); }); test('two', () => { expect(stringify({ name: 'zfpx', age: 9 })).toBe('name=zfpx&age=9'); });});2.5 配置2.5.1 配置位置package.jsonjest.config.js(推荐)命令行2.5.2 配置项testMatch glob规则,识别哪些文件中测试文件testRegex 文件正则testEnvironment 测试环境rootDir 根目录moduleFileExtensions 模块文件扩展名具体的配置项可参数后续的2.10附录 ...

September 9, 2019 · 2 min · jiezi

jest-报错-SyntaxError-Unexpected-string

最近研究单元测试,使用 vue-cli3 新建的项目,然后一直报错,其中错误如下 ● Test suite failed to run /Users/lyc/Desktop/study/vue/vue-jest1/tests/unit/about.spec.js:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import "core-js/modules/es6.array.find"; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Unexpected string at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:403:17)Test Suites: 1 failed, 1 passed, 2 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 1.14sRan all test suites.npm ERR! code ELIFECYCLEnpm ERR! errno 1npm ERR! vue-jest1@0.1.0 test:unit: `vue-cli-service test:unit`npm ERR! Exit status 1npm ERR! npm ERR! Failed at the vue-jest1@0.1.0 test:unit script.npm ERR! This is probably not a problem with npm. There is likely additional logging output above.npm ERR! A complete log of this run can be found in:npm ERR! /Users/lyc/.npm/_logs/2019-06-19T04_20_25_882Z-debug.log找了很久最终在 https://github.com/vuejs/vue-... 找到解决方案。 ...

June 19, 2019 · 1 min · jiezi

基于-TypeScript-开发-NPM-模块

初始化 NPM 项目mkdir project-namecd project-namenpm init添加开发基础包添加 TypeScript yarn add typescript -D添加 Jest 测试工具 yarn add jest ts-jest @types/jest -D添加 @types/node yarn add @types/node -D初始化 TypeScript 配置./node_modules/.bin/tsc --init这会在你的项目根目录新建一个 tsconfig.json 文件 现在的目录结构如下: .├── node_modules├── package.json├── tsconfig.json└── yarn.lock文件解析tsconfig.json这是 TypeScript 的配置文件,默认仅启用了几项,我一般的配置如下: { "compilerOptions": { /* Basic Options */ "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "lib": [ "es6" ] /* Specify library files to be included in the compilation. */, // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": true /* Generates corresponding '.d.ts' file. */, "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "incremental": true, /* Enable incremental compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "strictNullChecks": true /* Enable strict null checks. */, "strictFunctionTypes": true /* Enable strict checking of function types. */, "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }, "include": ["./src/**/*"], "exclude": ["node_modules", "src/__tests__"]}package.json添加了几条 scripts: ...

May 29, 2019 · 6 min · jiezi

vuetestutils初使用

vue-test-utils官网:https://vue-test-utils.vuejs....jest官网:https://jestjs.io 依赖包请安装它们???? yarn add @vue/test-utils vue-jestyarn jestjest-serializer-vueyarn add babel-jest babel-core@^7.0.0-bridge.0⚠️:vue-jest依赖与babel-core。我们的环境现在都是babel7,通过npm安装的babel-core默认的还是6版本,所以要指定babel-core安装的系列为7,否则会出现解析问题。 配置jest配置:告诉jest它需要哪些额外的配置jest相关的配置可以配置在package.json中或者单独的jest.config.json文件中: // jest.config.json{ "moduleFileExtensions": [ "js", "json", "vue" ], "transform": { "^.+\\.js$": "<rootDir>/node_modules/babel-jest", // jest使用babel解析js ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest" // jest对vue单文件的解析 }, "snapshotSerializers": [ "<rootDir>/node_modules/jest-serializer-vue" ], "moduleNameMapper": { "^@/(.*)$": "<rootDir>/src/$1", //为了解析webpack配置的alias字段 "^tim-architect/(.*)$": "<rootDir>/tim-architect/$1" }, "transformIgnorePatterns": [ "node_modules/(?!(yourModuleName))" ]}⚠️:transformIgnorePatterns的默认配置是["node_modules"],表示所有的node_modules下的包都不需要babel解析。但是一些3rd库提供的文件仍然是未编译的es6语法,jest在解析时会报语法错误。因此需要指定白名单,需要那些node_modules下的包被babel转换。 babel配置:告诉babel你需要用哪些工具去处理一坨(????香么 ????? : ???? )代码推荐使用babel.config.js(babel需要转换的node_modules同样生效)而不是.babelrc(当前项目生效)。 { ..., env: { test: { presets: [[ '@babel/env', { modules: 'auto', // 现在不能通过webpack来解析s6 module,需要使用babel来解析,所以要开启 targets: { node: 'current' // 指定环境为当前node版本,减少解析不识别语法的范围 } } ]], plugins: [[ '@babel/plugin-transform-runtime', { corejs: 2, useESModules: false // 不允许使用es modules,babel需要通过@babel/plugin-transform-modules-commonjs将es module转换为commonjs模块解析 } ] ] } }}⚠️:通过babel的env.test指定jest测试时需要的babel配置(同webpack转换解析时不同),jest会自动识别env.test的配置。 ...

May 23, 2019 · 1 min · jiezi

聊聊jest的IdleConnectionReaper

序本文主要研究一下jest的IdleConnectionReaper IdleConnectionReaperjest-common-6.3.1-sources.jar!/io/searchbox/client/config/idle/IdleConnectionReaper.java public class IdleConnectionReaper extends AbstractScheduledService { final static Logger logger = LoggerFactory.getLogger(IdleConnectionReaper.class); private final ReapableConnectionManager reapableConnectionManager; private final ClientConfig clientConfig; public IdleConnectionReaper(ClientConfig clientConfig, ReapableConnectionManager reapableConnectionManager) { this.reapableConnectionManager = reapableConnectionManager; this.clientConfig = clientConfig; } @Override protected void runOneIteration() throws Exception { logger.debug("closing idle connections..."); reapableConnectionManager.closeIdleConnections(clientConfig.getMaxConnectionIdleTime(), clientConfig.getMaxConnectionIdleTimeDurationTimeUnit()); } @Override protected Scheduler scheduler() { return Scheduler.newFixedDelaySchedule(0l, clientConfig.getMaxConnectionIdleTime(), clientConfig.getMaxConnectionIdleTimeDurationTimeUnit()); } @Override protected ScheduledExecutorService executor() { final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat(serviceName()) .build()); // Add a listener to shutdown the executor after the service is stopped. This ensures that the // JVM shutdown will not be prevented from exiting after this service has stopped or failed. // Technically this listener is added after start() was called so it is a little gross, but it // is called within doStart() so we know that the service cannot terminate or fail concurrently // with adding this listener so it is impossible to miss an event that we are interested in. addListener(new Listener() { @Override public void terminated(State from) { executor.shutdown(); } @Override public void failed(State from, Throwable failure) { executor.shutdown(); }}, MoreExecutors.directExecutor()); return executor; }}IdleConnectionReaper继承了AbstractScheduledService,它的构造器接收clientConfig及reapableConnectionManager;其runOneIteration执行了reapableConnectionManager.closeIdleConnections;其scheduler方法创建的是fixedDelay Scheduler;其executor方法创建的是SingleThreadScheduledExecutorReapableConnectionManagerjest-common-6.3.1-sources.jar!/io/searchbox/client/config/idle/ReapableConnectionManager.java ...

April 22, 2019 · 2 min · jiezi

聊聊jest的NodeChecker

序本文主要研究一下jest的NodeChecker NodeCheckerjest-common-6.3.1-sources.jar!/io/searchbox/client/config/discovery/NodeChecker.java public class NodeChecker extends AbstractScheduledService { private final static Logger log = LoggerFactory.getLogger(NodeChecker.class); private final static String PUBLISH_ADDRESS_KEY = "http_address"; private final static String PUBLISH_ADDRESS_KEY_V5 = "publish_address"; // The one that under "http" node private final static Pattern INETSOCKETADDRESS_PATTERN = Pattern.compile("(?:inet\\[)?(?:(?:[^:]+)?\\/)?([^:]+):(\\d+)\\]?"); private final NodesInfo action; protected JestClient client; protected Scheduler scheduler; protected String defaultScheme; protected Set<String> bootstrapServerList; protected Set<String> discoveredServerList; public NodeChecker(JestClient jestClient, ClientConfig clientConfig) { action = new NodesInfo.Builder() .withHttp() .addNode(clientConfig.getDiscoveryFilter()) .build(); this.client = jestClient; this.defaultScheme = clientConfig.getDefaultSchemeForDiscoveredNodes(); this.scheduler = Scheduler.newFixedDelaySchedule( 0l, clientConfig.getDiscoveryFrequency(), clientConfig.getDiscoveryFrequencyTimeUnit() ); this.bootstrapServerList = ImmutableSet.copyOf(clientConfig.getServerList()); this.discoveredServerList = new LinkedHashSet<String>(); } @Override protected void runOneIteration() throws Exception { JestResult result; try { result = client.execute(action); } catch (CouldNotConnectException cnce) { // Can't connect to this node, remove it from the list log.error("Connect exception executing NodesInfo!", cnce); removeNodeAndUpdateServers(cnce.getHost()); return; // do not elevate the exception since that will stop the scheduled calls. // throw new RuntimeException("Error executing NodesInfo!", e); } catch (Exception e) { log.error("Error executing NodesInfo!", e); client.setServers(bootstrapServerList); return; // do not elevate the exception since that will stop the scheduled calls. // throw new RuntimeException("Error executing NodesInfo!", e); } if (result.isSucceeded()) { LinkedHashSet<String> httpHosts = new LinkedHashSet<String>(); JsonObject jsonMap = result.getJsonObject(); JsonObject nodes = (JsonObject) jsonMap.get("nodes"); if (nodes != null) { for (Entry<String, JsonElement> entry : nodes.entrySet()) { JsonObject host = entry.getValue().getAsJsonObject(); JsonElement addressElement = null; if (host.has("version")) { int majorVersion = Integer.parseInt(Splitter.on('.').splitToList(host.get("version").getAsString()).get(0)); if (majorVersion >= 5) { JsonObject http = host.getAsJsonObject("http"); if (http != null && http.has(PUBLISH_ADDRESS_KEY_V5)) addressElement = http.get(PUBLISH_ADDRESS_KEY_V5); } } if (addressElement == null) { // get as a JsonElement first as some nodes in the cluster may not have an http_address if (host.has(PUBLISH_ADDRESS_KEY)) addressElement = host.get(PUBLISH_ADDRESS_KEY); } if (addressElement != null && !addressElement.isJsonNull()) { String httpAddress = getHttpAddress(addressElement.getAsString()); if(httpAddress != null) httpHosts.add(httpAddress); } } } if (log.isDebugEnabled()) { log.debug("Discovered {} HTTP hosts: {}", httpHosts.size(), Joiner.on(',').join(httpHosts)); } discoveredServerList = httpHosts; client.setServers(discoveredServerList); } else { log.warn("NodesInfo request resulted in error: {}", result.getErrorMessage()); client.setServers(bootstrapServerList); } } protected void removeNodeAndUpdateServers(final String hostToRemove) { log.warn("Removing host {}", hostToRemove); discoveredServerList.remove(hostToRemove); if (log.isInfoEnabled()) { log.info("Discovered server pool is now: {}", Joiner.on(',').join(discoveredServerList)); } if (!discoveredServerList.isEmpty()) { client.setServers(discoveredServerList); } else { client.setServers(bootstrapServerList); } } @Override protected Scheduler scheduler() { return scheduler; } @Override protected ScheduledExecutorService executor() { final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat(serviceName()) .build()); // Add a listener to shutdown the executor after the service is stopped. This ensures that the // JVM shutdown will not be prevented from exiting after this service has stopped or failed. // Technically this listener is added after start() was called so it is a little gross, but it // is called within doStart() so we know that the service cannot terminate or fail concurrently // with adding this listener so it is impossible to miss an event that we are interested in. addListener(new Listener() { @Override public void terminated(State from) { executor.shutdown(); } @Override public void failed(State from, Throwable failure) { executor.shutdown(); }}, MoreExecutors.directExecutor()); return executor; } /** * Converts the Elasticsearch reported publish address in the format "inet[<hostname>:<port>]" or * "inet[<hostname>/<hostaddress>:<port>]" to a normalized http address in the form "http://host:port". */ protected String getHttpAddress(String httpAddress) { Matcher resolvedMatcher = INETSOCKETADDRESS_PATTERN.matcher(httpAddress); if (resolvedMatcher.matches()) { return defaultScheme + resolvedMatcher.group(1) + ":" + resolvedMatcher.group(2); } return null; }}NodeChecker继承了AbstractScheduledService,它的构造器根据clientConfig的discoveryFrequency及discoveryFrequencyTimeUnit新建了fixedDelayScheduler来执行node checker;它实现了runOneIteration方法,该方法主要是发送NodesInfo请求(GET /_nodes/_all/http)如果请求抛出CouldNotConnectException则调用removeNodeAndUpdateServers方法移除该host;如果抛出其他的Exception则将client的servers重置为bootstrapServerList如果请求成功则解析body,如果nodes下面有version且大于等于5则取http节点下面的PUBLISH_ADDRESS_KEY_V5(publish_address)属性值添加到discoveredServerList;旧版本的则从nodes下面的PUBLISH_ADDRESS_KEY(http_address)属性值添加到discoveredServerListNodesInfo返回实例{ "_nodes" : { "total" : 1, "successful" : 1, "failed" : 0 }, "cluster_name" : "docker-cluster", "nodes" : { "RmyGhZEbTjC7JCQFVS3HWQ" : { "name" : "RmyGhZE", "transport_address" : "172.17.0.2:9300", "host" : "172.17.0.2", "ip" : "172.17.0.2", "version" : "6.6.2", "build_flavor" : "oss", "build_type" : "tar", "build_hash" : "3bd3e59", "roles" : [ "master", "data", "ingest" ], "http" : { "bound_address" : [ "0.0.0.0:9200" ], "publish_address" : "192.168.99.100:9200", "max_content_length_in_bytes" : 104857600 } } }}如果是5版本及以上的则在nodes下面有http属性,里头有publish_address属性用于返回该node的publish addressJestHttpClientjest-6.3.1-sources.jar!/io/searchbox/client/http/JestHttpClient.java ...

April 21, 2019 · 5 min · jiezi

聊聊springboot jest autoconfigure

序本文主要研究一下springboot jest autoconfigureJestPropertiesspring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/elasticsearch/jest/JestProperties.java@ConfigurationProperties(prefix = “spring.elasticsearch.jest”)public class JestProperties { /** * Comma-separated list of the Elasticsearch instances to use. / private List<String> uris = new ArrayList<>( Collections.singletonList(“http://localhost:9200”)); /* * Login username. / private String username; /* * Login password. / private String password; /* * Whether to enable connection requests from multiple execution threads. / private boolean multiThreaded = true; /* * Connection timeout. / private Duration connectionTimeout = Duration.ofSeconds(3); /* * Read timeout. / private Duration readTimeout = Duration.ofSeconds(3); /* * Proxy settings. / private final Proxy proxy = new Proxy(); //…… public static class Proxy { /* * Proxy host the HTTP client should use. / private String host; /* * Proxy port the HTTP client should use. */ private Integer port; public String getHost() { return this.host; } public void setHost(String host) { this.host = host; } public Integer getPort() { return this.port; } public void setPort(Integer port) { this.port = port; } }}JestProperties提供了uris、username、password、multiThreaded(默认true)、connectionTimeout(默认3s)、readTimeout(默认3s)、proxy的配置JestAutoConfigurationspring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfiguration.java@Configuration@ConditionalOnClass(JestClient.class)@EnableConfigurationProperties(JestProperties.class)@AutoConfigureAfter(GsonAutoConfiguration.class)public class JestAutoConfiguration { private final JestProperties properties; private final ObjectProvider<Gson> gsonProvider; private final ObjectProvider<HttpClientConfigBuilderCustomizer> builderCustomizers; public JestAutoConfiguration(JestProperties properties, ObjectProvider<Gson> gson, ObjectProvider<HttpClientConfigBuilderCustomizer> builderCustomizers) { this.properties = properties; this.gsonProvider = gson; this.builderCustomizers = builderCustomizers; } @Bean(destroyMethod = “shutdownClient”) @ConditionalOnMissingBean public JestClient jestClient() { JestClientFactory factory = new JestClientFactory(); factory.setHttpClientConfig(createHttpClientConfig()); return factory.getObject(); } protected HttpClientConfig createHttpClientConfig() { HttpClientConfig.Builder builder = new HttpClientConfig.Builder( this.properties.getUris()); PropertyMapper map = PropertyMapper.get(); map.from(this.properties::getUsername).whenHasText().to((username) -> builder .defaultCredentials(username, this.properties.getPassword())); Proxy proxy = this.properties.getProxy(); map.from(proxy::getHost).whenHasText().to((host) -> { Assert.notNull(proxy.getPort(), “Proxy port must not be null”); builder.proxy(new HttpHost(host, proxy.getPort())); }); map.from(this.gsonProvider::getIfUnique).whenNonNull().to(builder::gson); map.from(this.properties::isMultiThreaded).to(builder::multiThreaded); map.from(this.properties::getConnectionTimeout).whenNonNull() .asInt(Duration::toMillis).to(builder::connTimeout); map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::readTimeout); customize(builder); return builder.build(); } private void customize(HttpClientConfig.Builder builder) { this.builderCustomizers.orderedStream() .forEach((customizer) -> customizer.customize(builder)); }}JestAutoConfiguration在没有jestClient的bean情况下会通过JestProperties创建了HttpClientConfig,然后使用JestClientFactory创建JestClient,同时标记其destroyMethod为shutdownClient方法JestClientFactoryjest-6.3.1-sources.jar!/io/searchbox/client/JestClientFactory.javapublic class JestClientFactory { final static Logger log = LoggerFactory.getLogger(JestClientFactory.class); private HttpClientConfig httpClientConfig; public JestClient getObject() { JestHttpClient client = new JestHttpClient(); if (httpClientConfig == null) { log.debug(“There is no configuration to create http client. Going to create simple client with default values”); httpClientConfig = new HttpClientConfig.Builder(“http://localhost:9200”).build(); } client.setRequestCompressionEnabled(httpClientConfig.isRequestCompressionEnabled()); client.setServers(httpClientConfig.getServerList()); final HttpClientConnectionManager connectionManager = getConnectionManager(); final NHttpClientConnectionManager asyncConnectionManager = getAsyncConnectionManager(); client.setHttpClient(createHttpClient(connectionManager)); client.setAsyncClient(createAsyncHttpClient(asyncConnectionManager)); // set custom gson instance Gson gson = httpClientConfig.getGson(); if (gson == null) { log.info(“Using default GSON instance”); } else { log.info(“Using custom GSON instance”); client.setGson(gson); } // set discovery (should be set after setting the httpClient on jestClient) if (httpClientConfig.isDiscoveryEnabled()) { log.info(“Node Discovery enabled…”); if (!Strings.isNullOrEmpty(httpClientConfig.getDiscoveryFilter())) { log.info(“Node Discovery filtering nodes on "{}"”, httpClientConfig.getDiscoveryFilter()); } NodeChecker nodeChecker = createNodeChecker(client, httpClientConfig); client.setNodeChecker(nodeChecker); nodeChecker.startAsync(); nodeChecker.awaitRunning(); } else { log.info(“Node Discovery disabled…”); } // schedule idle connection reaping if configured if (httpClientConfig.getMaxConnectionIdleTime() > 0) { log.info(“Idle connection reaping enabled…”); IdleConnectionReaper reaper = new IdleConnectionReaper(httpClientConfig, new HttpReapableConnectionManager(connectionManager, asyncConnectionManager)); client.setIdleConnectionReaper(reaper); reaper.startAsync(); reaper.awaitRunning(); } else { log.info(“Idle connection reaping disabled…”); } Set<HttpHost> preemptiveAuthTargetHosts = httpClientConfig.getPreemptiveAuthTargetHosts(); if (!preemptiveAuthTargetHosts.isEmpty()) { log.info(“Authentication cache set for preemptive authentication”); client.setHttpClientContextTemplate(createPreemptiveAuthContext(preemptiveAuthTargetHosts)); } client.setElasticsearchVersion(httpClientConfig.getElasticsearchVersion()); return client; } public void setHttpClientConfig(HttpClientConfig httpClientConfig) { this.httpClientConfig = httpClientConfig; } //……}JestClientFactory的getObject方法首先创建JestHttpClient,然后设置HttpClient、AsyncClient如果isDiscoveryEnabled为true则会创建NodeChecker并执行Node Discovery如果maxConnectionIdleTime大于0则会创建IdleConnectionReaper,进行Idle connection reaping小结JestProperties提供了uris、username、password、multiThreaded(默认true)、connectionTimeout(默认3s)、readTimeout(默认3s)、proxy的配置JestAutoConfiguration在没有jestClient的bean情况下会通过JestProperties创建了HttpClientConfig,然后使用JestClientFactory创建JestClient,同时标记其destroyMethod为shutdownClient方法JestClientFactory的getObject方法首先创建JestHttpClient,然后设置HttpClient、AsyncClient;如果isDiscoveryEnabled为true则会创建NodeChecker并执行Node Discovery;如果maxConnectionIdleTime大于0则会创建IdleConnectionReaper,进行Idle connection reapingdocJestAutoConfiguration ...

April 20, 2019 · 3 min · jiezi

在 Angular 中引入 Jest 进行单元测试

在 Angular 中引入 Jest 进行单元测试为什么要从 Karma 迁移到 Jest用 Karma 在项目中遇到了坑最近新换了一个项目,去的时候项目已经做了两个月了,因为前期赶功能,没有对单元测试做要求,CI/CD 的时候也没有强制跑单元测试。所以虽然有用 Angular CLI 自动生成的测试文件,但是基本上都是测试不通过。项目做久了,人员变动多,新来的成员对之前的业务逻辑不清不楚,稍不注意就会破坏之前的功能;业务复杂了,随便增加或者修改一点点功能都可能引起不易被察觉的 BUG。作为一个敬业的开发,不上单元测试怎么行。所以,就有了一个修复已有单元测试的任务。修复已有测试文件的思路很简单:写个 TestingModule 把常用的依赖 mock 掉,再引入到需要的文件中就行了;不常用的依赖,在各自的文件中 mock 掉就好了。然而实际操作起来的时候,Karma 早早挖好坑等这了。有些测试文件单跑没有问题,整体跑得时候就报错,测试结果及其不稳定;karma 的报错信息又特别难读懂,很多时候根本定位不到到底是哪里出了问题。再加上 Karma 需要先把 Angular 应用编译之后再在浏览器中跑测试,整体时间也比较慢,修复的过程一直处于抓狂的边缘。整体测试跑起来的时候难以定位测试出错的定位,怎么办呢,那就让跑整个测试的时候各个文件之间也没有依赖可以单独跑好了,所以就想到了 Jest。实践证明,在 Angular 中, Jest 大法也非常好使。Karma 和 Jest 的对比前面也说过了,在修复测试的过程中,karma 遇到了各种各样的问题。归结起来大概就是:Karma 需要先把 Angular 应用整体编译之后再在浏览器中跑测试,跑测试的时间比较长;Karma 测试结果不稳定(很可能是因为异步操作引起的),单个文件和整体测试时的测试结果不一致;报错信息模糊不清,无法定位问题。特别是在有大量测试需要修复的情况下,难以定位问题的根本原因。那么对比而言,Jest 在上面这些方面都有很好的表现:不需要整体编译,可以单文件测试测试结果稳定报错清楚,易于定位问题除了这些,Jest 还有的好处有:开箱即用,基本算是全家桶,包含了测试需要的大部分工具:测试结构、断言、spies、mocks直接提供了测试覆盖率报告快照测试非常强大的模块级 mock 功能watch 模式仅仅测试和被修改文件相关的测试,速度非常快迁移第一步,你需要相关依赖包:npm install –save-dev jest jest-preset-angular @types/jest其中:jest – Jest 测试框架jest-preset-angular – jest 对于 angular 的一些通用的预设置@types/jest – Jest 的 typings第二步,你需要在 package.json 中对 Jest 进行配置:“jest”: { “preset”: “jest-preset-angular”, “setupFilesAfterEnv”: ["<rootDir>/src/setupJest.ts"]}其中,preset 声明了预设,setupFilesAfterEnv 配置了 Jest setup 文件的地址,可以包含多个文件,这里设置的是项目根目录下的 src/setupJest.ts。第三步,在 src 目录下创建上一步中设置的 setup 文件 setupJest.tsimport ‘jest-preset-angular’; // jest 对于 angular 的预配置import ‘./jestGlobalMocks’; // jest 全局的 mock第四步,在 src 目录下创建 jestGlobalMocks.ts 文件,并加入相关的全局的 mock,以下是一个例子:const mock = () => { let storage = {}; return { getItem: key => key in storage ? storage[key] : null, setItem: (key, value) => storage[key] = value || ‘’, removeItem: key => delete storage[key], clear: () => storage = {}, };};Object.defineProperty(window, ’localStorage’, {value: mock()});Object.defineProperty(window, ‘sessionStorage’, {value: mock()});Object.defineProperty(window, ‘getComputedStyle’, { value: () => [’-webkit-appearance’]});可以看到这个例子中 mock 了 window 上的对象,这是因为 jsdom 并没有实现所有的 window 上的对象和方法,所以有时我们需要自己给 window 打个补丁。在这里 mock localStorage 是可选的,如果我们在代码中并没有使用。但是 mock getComputedStyle 是必须的,因为 Angular 会检查它在哪个浏览器中执行。如果没有 mock getComputedStyle,我们的测试代码将无法执行。接下来,我们就可以在 package.json 的 script 中配置 test 的命令了:“test”: “jest”,“test:watch”: “jest –watch”,其中 test 只跑一次测试,test:watch 可以检测文件变化,跑当前有修改的文件的相关测试。此时,在命令行中运行测试命令,就应该能够顺利把测试跑起来并通过了。如果没有通过,可能是因为我们在 src/tsconfig.spec.json 中的 file 配置中有 test.js 的配置,这是 Karma 的 setup 文件,删掉这行配置并删除对应的文件,(src/tsconfig.app.json 中出现的 test.js 也可一并删除),重新跑一遍测试命令:npm run test至此,Jest 测试环境就算顺利搭建好了。如果你对代码有洁癖,接下来,你还可以删除 Karma 的相关代码,将测试全部转为 Jest。删除 Karma 相关代码删除相关依赖包(@types/jasmine @types/jasminewd2 jasmine-core jasmine-spec-reporter 因为在 e2e 测试中有使用所以不能删除):npm uninstall karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter删除文件 src/karma.config.js删除 angular.json 中 test 的配置src/tsconfig.spec.json 中 compilerOptions.type 的配置移除 jasmine, 加上 jest。至此,你已经删除了所有与 Karma 相关的代码。你甚至还能将测试断言换成 jest 的风格。查看最后生成的代码库和相关文件配置参考:Angular 6: “ng test” with Jest in 3 minutesTESTING ANGULAR FASTER WITH JEST ...

March 30, 2019 · 2 min · jiezi

React 测试指南

前端测试金字塔对于一个 Web 应用来说,理想的测试组合应该包含大量单元测试(unit tests),部分快照测试(snapshot tests),以及少量端到端测试(e2e tests)。参考测试金字塔,我们构建了前端应用的测试金字塔。单元测试针对程序模块进行测试。模块是软件设计中的最小单位,一个函数或者一个 React 组件都可以称之为一个模块。单元测试运行快,反馈周期短,在短时间内就能够知道是否破坏了代码,因此在测试组合中占据了绝大部分。快照测试对组件的 UI 进行测试。传统的快照测试会拍摄组件的图片,并且将它和之前的图片进行对比,如果两张图片不匹配则测试失败。Jest 的快照测试不会拍摄图片,而是将 React 树序列化成字符串,通过比较两个字符串来判断 UI 是否改变。因为是纯文本的对比,所以不需要构建整个应用,运行速度自然比传统快照测试更快。E2E 测试相当于黑盒测试。测试者不需要知道程序内部是如何实现的,只需要根据业务需求,模拟用户的真实使用场景进行测试。技术选型测试种类技术选型单元测试Jest + Enzyme快照测试JestE2E 测试jest-puppeteerJest 是 Facebook 开源的测试框架。它的功能很强大,包含了测试执行器、断言库、spy、mock、snapshot 和测试覆盖率报告等。Enzyme 是 Airbnb 开源的 React 单元测试工具。它扩展了 React 官方的 TestUtils,通过类 jQuery 风格的 API 对 DOM 进行处理,减少了很多重复代码,可以很方便的对渲染出来的结果进行断言。jest-puppeteer 是一个同时包含 Jest 和 Puppeteer 的工具。Puppeteer 是谷歌官方提供的 Headless Chrome Node API,它提供了基于 DevTools Protocol 的上层 API 接口,用来控制 Chrome 或者 Chromium。有了 Puppeteer,我们可以很方便的进行端到端测试。React 测试策略测试本质上是对代码的保护,保证项目在迭代的过程中正常运行。当然,写测试也是有成本的,特别是复杂逻辑,写测试花的时间,可能不比写代码少。所以我们要制定合理的测试策略,有针对性的去写测试。至于哪些代码要测,哪些代码不测,总的来说遵循一个原则:投入低,收益高。「投入低」是指测试容易写,「收益高」是测试的价值高。换句话说,就是指测试应该优先保证核心代码逻辑,比如核心业务、基础模块、基础组件等,同时,编写测试和维护测试的成本也不宜过高。当然,这是理想情况,在实际的开发过程中还是要进行权衡。单元测试基于 React 和 Redux 项目的特点,我们制定了下面的测试策略:分类哪些要测?哪些不测?组件 有条件渲染的组件(如 if-else 分支,联动组件,权限控制组件等) 有用户交互的组件(如 Click、提交表单等)* 逻辑组件(如高阶组件和 Children Render 组件) connect 生成的容器组件 纯组合子组件的 Page 组件 纯展示的组件 组件样式Reducer有逻辑的 Reducer。如合并、删除 state。纯取值的 reducer 不测。比如(_, action) => action.payload.data Middleware全测无Action Creator无全不测方法 validators formatters* 其他公有方法私有方法公用模块全测。比如处理 API 请求的模块。无Note: 如果使用了 TypeScript,类型约束可以替代部分函数入参和返回值类型的检查。快照测试Jest 的 snapshot 测试虽然运行起来很快,也能够起到一定保护 UI 的作用。但是它维护起来很困难(大量依赖人工对比),并且有时候不稳定(UI 无变化但 className 变化仍然会导致测试失败)。因此,个人不推荐在项目中使用。但是为了应付测试覆盖率,以及「给自己信心」,也可以给以下部分添加 snapshot 测试:Page 组件:一个 page 对应一个 snapshot。纯展示的公用 UI 组件。快照测试可以等整个 Page 或者 UI 组件构建完成之后再添加,以保证稳定。E2E 测试覆盖核心的业务 flow。一个好的单元测试应该具备的条件?安全重构已有代码单元测试一个很重要的价值是为重构保驾护航。当输入不变时,当且仅当「被测业务代码功能被改动了」时,测试才应该挂掉。也就是说,无论怎么重构,测试都不应该挂掉。在写组件测试时,我们常常遇到这样的情况:用 css class 选择器选中一个节点,然后对它进行断言,那么即使业务逻辑没有发生变化,重命名这个 class 时也会使测试挂掉。理论上来说,这样的测试并不算一个「好的测试」,但是考虑到它的业务价值,我们还是会写一些这样的测试,只不过写测试的时候需要注意:使用一些不容易发生变化的选择器,比如 component name、arial-label 等。保存业务上下文我们经常说测试即文档,没错,一个好的测试往往能够非常清晰的表单业务或代码的含义。快速回归快速回归是指测试运行速度快,且稳定。要想运行速度快,很重要的一点是 mock 好外部依赖。至于怎么具体怎么 mock 外部依赖,后面会详细说明。单元测试怎么写?定义测试名称建议采用 BDD 的方式,即测试要接近自然语言,方便团队中的各个成员进行阅读。编写测试用例的时候,可以参考 AC,试着将 AC 的 Give-When-Then 转化成测试用例。GIVEN: 准备测试条件,比如渲染组件。WHEN:在某个具体的场景下,比如点击 button。THEN:断言describe(“add user”, () => { it(“when I tap add user button, expected dialog opened with 3 form fields”, () => { // Given: in profile page. // Prepare test env, like render component etc. // When: button click. // Simulate button click // Then: display add user form, which contains username, age and phone number. // Assert form fields length to equal 3 });});Mock 外部依赖单元测试的一个重要原则就是无依赖和隔离。也就是说,在测试某部分代码时,我们不期望它受到其他代码的影响。如果受到外部因素影响,测试就会变得非常复杂且不稳定。我们写单元测试时,遇到的最大问题就是:代码过于复杂。比如当页面有 API 请求、日期、定时器或 redux conent 时,写测试就变得异常困难,因为我们需要花大量时间去隔离这些外部依赖。隔离外部依赖需要用到测试替代方法,常见的有 spies、stubs 和 mocks。很多测试框架都实现了这三种方法,比如著名的 Jest 和 Sinon。这些方法可以帮助我们在测试中替换代码,减少测试编写的复杂度。spiesspies 本质上是一个函数,它可以记录目标函数的调用信息,如调用次数、传参、返回值等等,但不会改变原始函数的行为。Jest 中的 mock function 就是 spies,比如我们常用的 jest.fn() 。// Example:onSubmit() { // some other logic here this.props.dispatch(“xxx_action”);}// Example Test:it(“when form submit, expected dispatch function to be called”, () => { const mockDispatch = jest.fn(); mount(<SomeComp dispatch={mockDispatch}/>); // simlate submit event here expect(mockDispatch).toBeCalledWith(“xxx_action”); expect(mockDispatch).toBeCalledTimes(1);});spies 还可以用于替换属性方法、静态方法和原型链方法。由于这种修改会改变原始对象,使用之后必须调用 restore 方法予以还原,因此使用的时候要特别小心。// Example:const video = { play() { return true; },};// Example Test:test(‘plays video’, () => { const spy = jest.spyOn(video, ‘play’); const isPlaying = video.play(); expect(spy).toHaveBeenCalled(); expect(isPlaying).toBe(true); spy.mockRestore();});stubsstubs 跟 spies 类似,但与 spies 不同的是,stubs 会替换目标函数。也就是说,如果使用 spies,原始的函数依然会被调用,但使用 stubs,原始的函数就不会被执行了。stubs 能够保证明确的测试边界。它可以用于以下场景:替换让测试变得复杂或慢的外部函数,如 ajax。测试异常条件,如抛出异常。Jest 中也提供了类似的 API [](https://jestjs.io/docs/en/jes...[]()jest.spyOn().mockImplementation(),如下:const spy = jest.fn();const payload = [1, 2, 3];jest .spyOn(jQuery, “ajax”) .mockImplementation(({ success }) => success(payload));jQuery.ajax({ url: “https://example.api”, success: data => spy(data)});expect(spy).toHaveBeenCalledTimes(1);expect(spy).toHaveBeenCalledWith(payload);mocksmocks 是指用自定义对象代替目标对象。我们不仅可以 mock API 返回值和自定义类,还可以 mock npm 模块等等。// mock middleware apiconst mockMiddlewareAPI = { dispatch: jest.fn(), getState: jest.fn(),};// mock npm module configjest.mock(“config”, () => { return { API_BASE_URL: “http://base_url”, };});使用 mocks 时,需要注意:如果 mock 了某个模块的依赖,需要等 mock 完成了之后再 require 这个模块。有如下代码:// counter.tslet count = 0;export const get = () => count;export const inc = () => count++;export const dec = () => count–;错误做法:// counter.test.tsimport * as counter from “../counter”;describe(“counter”, () => { it(“get”, () => { jest.mock("../counter", () => ({ get: () => “mock count”, })); expect(counter.get()).toEqual(“mock count”); // 测试失败,此时的 counter 模块并非 mock 之后的模块。 });});正确做法:describe(“counter”, () => { it(“get”, () => { jest.mock("../counter", () => ({ get: () => “mock count”, })); const counter = require("../counter"); // 这里的 counter 是 mock 之后的 counter expect(counter.get()).toEqual(“mock count”); // 测试成功 });});多个测试有共享状态时,每次测试完成之后需要重置模块 jest.resetModules() 。它会清空所有 required 模块的缓存,保证模块之间的隔离。错误的做法:describe(“counter”, () => { it(“inc”, () => { const counter = require("../counter"); counter.inc(); expect(counter.get()).toEqual(1); }); it(“get”, () => { const counter = require("../counter"); // 这里的 counter 和上一个测试中的 counter 是同一份拷贝 expect(counter.get()).toEqual(0); // 测试失败 console.log(counter.get()); // ? 输出: 1 });});正确的做法:describe(“counter”, () => { afterEach(() => { jest.resetModules(); // 清空 required modules 的缓存 }); it(“inc”, () => { const counter = require("../counter"); counter.inc(); expect(counter.get()).toEqual(1); }); it(“get”, () => { const counter = require("../counter"); // 这里的 counter 和上一个测试中的 counter 是不同的拷贝 expect(counter.get()).toEqual(0); // 测试成功 console.log(counter.get()); // ? 输出: 0 });});修改代码,从一个外部模块 defaultCount 中获取 count 的默认值。// defaultCount.tsexport const defaultCount = 0;// counter.tsimport {defaultCount} from “./defaultCount”;let count = defaultCount;export const inc = () => count++;export const dec = () => count–;export const get = () => count;测试代码:import * as counter from “../counter”; // 首次导入 counter 模块console.log(counter); describe(“counter”, () => { it(“inc”, () => { jest.mock("../defaultCount", () => ({ defaultCount: 10, })); const counter1 = require("../counter"); // 再次导入 counter 模块 counter1.inc(); expect(counter1.get()).toEqual(11); // 测试失败 console.log(counter1.get()); // 输出: 1 });});再次 require counter 时,发现模块已经被 require 过了,就直接从缓存中获取,所以 counter1 使用的还是counter 的上下文,也就是 defaultCount = 0。而调用 resetModules() 会清空 cache,重新调用模块函数。在上面的代码中,注释掉 1,2 行,测试也会成功。大家可以想想为什么?编写测试组件测试渲染组件要对组件进行测试,首先要将组件渲染出来。Enzyme 提供了三种渲染方式: 浅渲染、全渲染以及静态渲染。浅渲染(Shallow Render)shallow 方法会把组件渲染成 Virtual DOM 对象,只会渲染组件中的第一层,不会渲染它的子组件,因此不需要关心 DOM 和执行环境,测试的运行速度很快。浅渲染对上层组件非常有用。上层组件往往包含很多子组件(比如 App 或 Page 组件),如果将它的子组件全部渲染出来,就意味着上层组件的测试要依赖于子组件的行为,这样不仅使测试变得更加困难,也大大降低了效率,不符合单元测试的原则。浅渲染也有天生的缺点,因为它只能渲染一级节点。如果要测试子节点,又不想全渲染怎么办呢?shallow 还提供了一个很好用的接口 .dive,通过它可以获取 wrapper 子节点的 React DOM 结构。示例代码:export const Demo = () => ( <CompA> <Container><List /></Container> </CompA>);使用 shallow 后得到如下结构:<CompA> <Container /></CompA>使用 .dive() 后得到如下结构:<div> <Container> <List /> </Container></div>全渲染(Full DOM Render)mount 方法会把组件渲染成真实的 DOM 节点。如果你的测试依赖于真实的 DOM 节点或者子组件,那就必须使用 mount 方法。特别是大量使用 Child Render 的组件,很多时候测试会依赖 Child Render 里面的内容,因此需要需要用全渲染,将子组件也渲染出来。全渲染方式需要浏览器环境,不过 Jest 已经提供了,它的默认的运行环境 jsdom ,就是一个 JavaScript 浏览器环境。需要注意的是,如果多个测试依赖了同一个 DOM,它们可能会相互影响,因此在每个测试结束之后,最好使用 .unmount() 进行清理。静态渲染(Static Render)将组件渲染成静态的 HTML 字符串,然后使用 Cheerio 对其进行解析,返回一个 Cheerio 实例对象,可以用来分析组件的 HTML 结构。测试条件渲染我们常常会用到条件渲染,也就是在满足不同条件时,渲染不同组件。比如: import React, { ReactNode } from “react”;const Container = ({ children }: { children: ReactNode }) => <div aria-label=“container”>{children}</div>;const CompA = ({ children }: { children: ReactNode }) => <div>{children}</div>;const List = () => <div>List Component</div>;interface IDemoListProps { list: string[];}export const DemoList = ({ list }: IDemoListProps) => ( <CompA> <Container>{list.length > 0 ? <List /> : null}</Container> </CompA>);对于条件渲染,这里提供了两种思路:测试是否渲染了正确节点一般的做法是将 DemoList 组件渲染出来,再根据不同的条件,去检查是否渲染出了正确的节点。describe(“DemoList”, () => { it(“when list length is more than 0, expected to render List component”, () => { const wrapper = shallow(<DemoList list={[“A”, “B”, “C”]} />); expect( wrapper .dive() .find(“List”) .exists(), ).toBe(true); }); it(“when list length is more than 0, expected to render null”, () => { const wrapper = shallow(<DemoList list={[]} />); expect( wrapper .dive() .find("[aria-label=‘container’]") .children().length, ).toBe(0); });});公用组件 + 只测判断条件我们可以抽象一个公用组件 <Show/> ,用于所有条件渲染的组件。这个组件接受一个 condition ,当满足这个 condition 时显示某个节点,不满足时显示另一个节点。<Show condition={} ifNode={} elseNode={} />我们可以为这个组件添加测试,确保在不同的条件下显示正确的节点。既然这个逻辑得已经得到了保证,使用 <Show/> 组件的地方就无需再次验证。因此我们只需要测试是否正确生成了 condition 即可。export const shouldShowBtn = (a: string, b: string, c: string) => a === b || b === c;describe(“should show button or not”, () => { it(“should show button”, () => { expect(shouldShowBtn(“x”, “x”, “x”)).toBe(true); }); it(“should hide button”, () => { expect(shouldShowBtn(“x”, “y”, “z”)).toBe(false); });});对于有权限控制的组件,一个小的配置改变也会导致整个渲染的不同,而且人工测试很难发现,这种配置多一个 prop 检查会让代码更加安全。测试用户交互常见的有点击事件、表单提交、validate 等。点击事件 click。onSubmit 。主要是测试 onSubmit 方法被调用之后是否发生了正确的行为,如 dispatch action 。validate 。 主要是测试 error message 是否按正确的顺序显示。Action Creator 测试action creator 的实现和测试都非常简单,这里就不举例了。但要注意的是,不要将计算逻辑放到 aciton creator 中。错误的方式:// action.tsexport const getList = createAction("@@list/getList", (reqParams: any) => { const params = formatReqParams({ …reqParams, page: reqParams.page + 1, startDate: formatStartDate(reqParams.startDate) endDate: formatStartDate(reqParams.endDate) }); return { url: “/api/list”, method: “GET”, params, };});正确的方式:// action.tsexport const getList = createAction("@@list/getList", (params: any) => { return { url: “/api/list”, method: “GET”, params, };});// 调用 action creator 时,先把值计算好,再传给 action creator。// utils.tsconst formatReqParams = (reqParams: any) => {return formatReqParams({ …reqParams, page: reqParams.page + 1, startDate: formatStartDate(reqParams.startDate) endDate: formatStartDate(reqParams.endDate) });};// page.tsgetFeedbackList(formatReqParams({}));Reducer 测试Reducer 测试主要是测试「根据 Action 和 State 是否生成了正确的 State」。因为 reducer 是纯函数,所以测试非常好写,这里就不细讲了。 Middleware 测试测试 middleware 最重要的就是 mock 外部依赖,其中包括 middlewareAPI 和 next 。Test Helper:class MiddlewareTestHelper { static of(middleware: any) { return new MiddlewareTestHelper(middleware); } constructor(private middleware: Middleware) {} create() { const middlewareAPI = { dispatch: jest.fn(), getState: jest.fn(), }; const next = jest.fn(); const invoke$ = (action: any) => this.middleware(middlewareAPI)(next)(action); return { middlewareAPI, next, invoke$, }; }}Example Test:it(“should handle the action”, () => { const { next, invoke$ } = MiddlewareTestHelper.of(testMiddleware()).create(); invoke$({ type: “SOME_ACTION”, payload: {}, }); expect(next).toBeCalled();});测试异步代码默认情况下,一旦到达运行上下文底部,jest测试立即结束。为了解决这个问题,我们可以使用:done() 回调函数return promiseasync/await错误的方式:test(’the data is peanut butter’, () => { function callback(data) { expect(data).toBe(‘peanut butter’); } fetchData(callback);});正确的方式:test(’the data is peanut butter’, done => { function callback(data) { expect(data).toBe(‘peanut butter’); done(); } fetchData(callback);});test(’the data is peanut butter’, () => { expect.assertions(1); return fetchData().then(data => { expect(data).toBe(‘peanut butter’); });});test(“the data is peanut butter”, async () => { const data = await fetchData(); expect(data).toBe(“peanut butter”);});执行测试采用「红 - 绿」的方式,即先让测试失败,再修改代码让测试通过,以确保断言被执行。快照测试怎么写?通过 redux-mock-store,将组件需要的全部数据准备好(给 mock store 准备 state),再进行测试。从测试的角度反思应用设计「好测试」的前提是要有「好代码」。因此我们可以从测试的角度去反思整个应用的设计,让组件的「可测试性」更高。单一职责。 一个组件只干一类事情,降低复杂度。只要每个小的部分能够被正确验证,组合起来能够完成整体功能,那么测试的时候,只需要专注于各个小的部分即可。良好的复用。 即复用逻辑的同时,也复用了测试。保证最小可用,再逐渐增加功能。 也就是我们平时所说的 TDD。…Debugconsole.log(wrapper.debug());参考文章 译-Sinon入门:利用Mocks,Spies和Stubs完成javascript测试使用Jest进行React单元测试对 React 组件进行单元测试How to Rethink Your Testing使用Enzyme测试React(Native)组件Node.js模块化机制原理探究单元测试的意义、做法、经验React 单元测试策略及落地 ...

January 30, 2019 · 6 min · jiezi

Jest + Enzyme 前端自动化测试

Jest、Enzyme 简介Jest 是 Facebook 发布的一个开源的、基于 Jasmine 框架的 JavaScript 单元测试工具。Enzyme 是 React 的测试类库。 Enzyme 提供了一套简洁强大的 API,并通过 jQuery 风格的方式进行DOM 处理,开发体验十分友好。普通方法测试首先,使用npm安装Jest npm install –save-dev jest在目录下新建一个待测试文件 sort.js。 function sort(sortArr) { return sortArr.sort((a, b) => a - b); } module.exports = sort;此处sort方法未对入参做类型检测在这里定义了一个数组排序方法,下面来书写其测试用例,在目录下新建一个sort.test.js文件。 const sort = require(’./sort’); const arr = [5,2,4,3,1]; test(‘排序数组[5,2,4,3,1]’, () => { expect(sort(arr)).toEqual([1,2,3,4,5]); })在用例中,我们先引入了待测试的方法,接下来定义了一个排序数组[5,2,4,3,1]的测试用例.test()用来定义一个测试用例,expect()会执行内部的方法,返回一个待测试的结果。toEqual()用来判断返回的结果于期望的结果是否相等。这里由于期望返回结果为数组,所以使用toEqual进行判断,除此之外,还有toBe(),toBeNull()等方法来比较不同的类型。更多内容…打开package.json,在scripts中新增 test: “jest"然后运行命令 npm run test会看到用例测试通过的信息由于我们的方法没有做入参类型检测,下面通过传入字符串,来测试异常情况。在sort.test.js中新增一个测试用例用例 test(‘排序字符串“52431”’, () => { expect(sort(‘52431’)).toEqual(12345); })运行,则会看到测试失败的信息从测试结果中我们可以清除的看到,运行来两个测试用例,第一个用例通过来,第二个用例运行是js出现了报错。此时便能根据测试结果,调整代码更多测试方法此处不做讨论,具体可以参考Jest文档在具体项目中的使用下面来在实际的项目中使用Jest + Enzyme来进行测试。测试Demo项目首先,使用Create-React-App来创建一个应用。接着,安装jest npm install –save-dev jest由于在书写用例时,会用到es6语法,所以还要安装babel-jest来进行转码 npm install –save-dev babel-jest安装enzyme npm install –save-dev enzyme也可以使用react官方测试插件react-addons-test-utils,此处我们使用enzyme,故不需要安装。此外,还需要根据使用的react版本来安装enzyme-adapter-react。具体版本对照如下enzyme-adapter-react版本react版本enzyme-adapter-react-16^16.4.0-0enzyme-adapter-react-16.316.3.0-0enzyme-adapter-react-16.216.2enzyme-adapter-react-16.1~16.0.0-0 \\~16.1enzyme-adapter-react-15^15.5.0enzyme-adapter-react-15.415.0.0-0 - 15.4.xenzyme-adapter-react-14^0.14.0enzyme-adapter-react-13^0.13.0此处demo使用的react版本为^16.4.1,所以我们需要安装enzyme-adapter-react-16 npm install –save-dev enzyme-adapter-react-16依赖安装完成,接下来需要进行相关的配置。首先配置package.json的测试命令test: “jest”。此时如果我们在根目录下创建一个.test.js文件,并书写简单的方法用例,执行测试命令,是可以正常执行测试用例的。但是,我们的项目却并不是简单的单个方法但测试,实际项目中会存在这大量的组件依赖,还有css,image等静态资源的处理。所以,还要进行如下配置处理。首先,我们在package.json文件中新增一个jest的配置项 jest: {}这里我们主要进行三个配置。moduleFileExtensions代表支持加载的文件名。此处我们的测试文件均以.js结尾,所以只配置成[“js”]即可transform用于编译 ES6/ES7 语法,需配合 babel-jest 使用moduleNameMapper代表需要被 Mock 的资源名称。如果需要 Mock 静态资源(如less、scss等),则需要配置 Mock 的路径jest默认会检索项目内的*.test.js,.test.jsx形式的文件并执行。当编写当用例没被jest检索到时,可通过moduleDirectories来配置路径。在具体到组件测试时,为了测试组件到交互性,我们需要jest渲染出组件进行操作,此时,由于我们到项目中大量使用来webpack到依赖管理,以及less-loader、url-loader等预编译。在jest渲染组件是,无法识别这些.less等文件。所以我们需要通过mock来处理这些静态文件。因为jest在渲染组件时,是不需要依赖css,image等静态资源的。所以我们可以这样配置: “\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$”: “<rootDir>/mocks/fileMock.js”, “\.(css|less)$”: “<rootDir>/mocks/styleMock.js"前面通过正则来适配我们需要匹配的静态文件,后面为我们通过mock返回的数据。这里我们还需要在根目录中创建__mock__的文件夹。在里面新建fileMock.js和styleMock.js两个文件。 <!–fileMock.js–> module.exports = ’test-file-stub’; <!–styleMock.js–> module.exports = {};这样就可以将测试集中在组件的结构和逻辑上。另外,可能在我们的项目中,会使用大量的别名来简化引用路径,及webpack中的alias配置。此处同样需要进行别名的配置,配置方式与静态资源配置类似。一下是完整配置 “jest”: { “moduleFileExtensions”: [ “js”, “jsx” ], “moduleDirectories”: [ “src”, “node_modules” ], “transform”: { “^.+\.js$”: “babel-jest” }, “moduleNameMapper”: { “^components(.)$”: “<rootDir>/src/components$1”, “^pages(.)$”: “<rootDir>/src/pages$1”, “^utils(.)$”: “<rootDir>/src/utils$1”, “^services(.)$”: “<rootDir>/src/services$1”, “^static(.)$”: “<rootDir>/src/static$1”, “^models(.)$”: “<rootDir>/src/models$1”, “^variable(.)$”: “<rootDir>//src/static/less/variable.less”, “\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$”: “<rootDir>/mocks/fileMock.js”, “\.(css|less)$”: “<rootDir>/mocks/styleMock.js” } }接下来创建一个待测试的组件,在src > pages文件夹中创建login组件,并配置好路由。组件代码参考测试Demo项目运行后页面如下<img src=“https://github.com/ISummerRai...; width=“320” />接着,定义测试用例,此Demo定义来八个测试用例如下1、页面title显示“登录”(UI)2、登录账号输入手机号或邮箱时,账号上方显示登录账号3、登录账号输入不为手机号或邮箱,账号上方显示【账户输入错误,请重新输入】4、账号输入正常,密码小于6位,登录按钮置灰。5、账号输入异常,密码不小于6位,登录按钮置灰。6、账号输入正常,密码不小于6位,登录按钮可点。7、点击密码后眼睛图标,显示密码。8、显示密码状态,再次点击,隐藏密码。接下来,新建文件login.test.js来编写测试用例代码。由于用例中设计多个交互,所以我们需要先渲染出组件。Enzyme为我们提供来三种渲染组件的方法shallow、render、mount。shallow 方法就是官方的shallow rendering的封装。render 方法将React组件渲染成静态的HTML字符串,然后分析这段HTML代码的结构,返回一个对象。它跟shallow方法非常像,主要的不同是采用了第三方HTML解析库Cheerio,它返回的是一个Cheerio实例对象。mount方法用于将React组件加载为真实DOM节点。三种方法中,shallow render 返回的为对象,用于分析HTML结构,所以无法用于交互测试。mount方法加载的为真实的DOM节点,所以可用于交互测试。本Login组件存在大量交互测试,所以使用mount创建组件,使用mount需要先使用Adapter配置如下 import Login from ‘pages/Login’; import React from ‘react’; import { configure } from ’enzyme’; import Adapter from ’enzyme-adapter-react-16’; import { mount } from ’enzyme’; configure({ adapter: new Adapter() }); const wrapper = mount(<Login />);现在,我们就可以使用Enzyme的API来编写测试用例了,Enzyme提供了丰富的类jquery风格的API,下面是部分API .get(index):返回指定位置的子组件的DOM节点 .at(index):返回指定位置的子组件 .first():返回第一个子组件 .last():返回最后一个子组件 .type():返回当前组件的类型 .text():返回当前组件的文本内容 .html():返回当前组件的HTML代码形式 .props():返回根组件的所有属性 .prop(key):返回根组件的指定属性 .state([key]):返回根组件的状态 .setState(nextState):设置根组件的状态 .setProps(nextProps):设置根组件的属性完整API参见 Enzyme API在前半部分的demo中,我们使用来 test() 方法来编写用例,此处,我们使用 describe(’’, () => { it(’’, () => {}) })来编写测试用例,这样我们可以对测试用例进行分组让我们来开始第一个用例“页面title显示「登录」”的编写 it(‘标题显示’, () => { const title = wrapper.find(’.title’).text(); expect(title).toBe(‘登录’); })这个用例十分简单,仅仅在第一步获取到了title中的文本,并对文本进行校验。第二个和第三个用例为对输入框输入文本对校验,此处,我们可以单独对校验方法进行测试,也可以页面对交互来完成测试。这里用例通过交互来进行测试用例对编写。由于在输入信息过程中,校验通过input框的onChange事件触发,所以我们需要用到 simulate 来触发事件。其中一个用例如下 const accountInput = wrapper.find(’.account’).find(‘input’); const accountTitle = wrapper.find(’.account .name’).find(‘span’); it(‘输入不合法账号’, () => { const event = { target: { value: ‘abc123’ } } accountInput.simulate(‘change’, event); expect(accountTitle.text()).toBe(‘账户输入错误,请重新输入’); })模拟输入来一个不合法的账号‘abc123’,验证失败,显示失败信息。在4,5,6三个用例中,需要获取登录按钮Button组件的可点击状态,由于enzyme无法获取 css 状态,此时可以使用API中的prop(key)来获取组件的props状态,从而判断组件的可点击状态。其中一个用例如下 it(‘输入正确账号,密码小于6位,指定状态’, () => { wrapper.setState({ account: ‘18888888888’, password: ‘12345’, errorAccount: false }); // 此处需重新获取btn对象,否则会导致用例失败 const submitBtn = wrapper.find(’.btn-box’).find(‘Button’); expect(submitBtn.props().disabled).toBe(true); })此处通过直接设置state的值来更改Button的状态。需要注意的是,为来减少重复定义,许多Dom对象的获取都在describe组下做了统一的定义,但在执行expect获取按钮状态是,需要重新查找,来获取最新但状态。除了直接指定state状态之外,还可以通过输入框输入,change事件触发但方式来完成用例,如下 it(‘输入正确账号,密码小于6位,通过change触发’, () => { const accountEvent = { target: { value: ‘18888888888’ } }; const pwdEvent = { target: { value: ‘12345’ } } accountInput.simulate(‘change’, accountEvent); passwordInput.simulate(‘change’, pwdEvent); const submitBtn = wrapper.find(’.btn-box’).find(‘Button’); expect(submitBtn.prop(‘disabled’)).toBe(true);7、8两个用例使用但方法与上面相同,不再赘述。所有用例编写完成之后,执行npm run test可以看到所有用例都通过测试。测试覆盖率在 package.json 文件的 test 命令修改为 test: “jest –coverage"执行 npm run test即可在用例执行信息后显示用例的覆盖率报告。 ...

January 17, 2019 · 2 min · jiezi

在Vue项目中使用snapshot测试

在Vue项目中使用snapshot测试snapshot介绍snapshot测试又称快照测试,可以直观地反映出组件UI是否发生了未预见到的变化。snapshot如字面上所示,直观描述出组件的样子。通过对比前后的快照,可以很快找出UI的变化之处。第一次运行快照测试时会生成一个快照文件。之后每次执行测试的时候,会生成一个快照,然后对比最初生成的快照文件,如果没有发生改变,则通过测试。否则测试不通过,同时会输出结果,对比不匹配的地方。jest中的快照文件以为snap拓展名结尾,格式如下(ps: 在没有了解之前,我还以为是快照文件是截图)。一个快照文件中可以包含多个快照,快照的格式其实是HTML字符串,对于UI组件,其HTML会反映出其内部的state。每次测试只需要对比字符串是否符合初始快照即可。exports[button 1] = "&lt;div&gt;&lt;span class=\\"count\\"&gt;1&lt;/span&gt; &lt;button&gt;Increment&lt;/button&gt; &lt;button class=\\"desc\\"&gt;Descrement&lt;/button&gt; &lt;button class=\\"custom\\"&gt;not emitted&lt;/button&gt;&lt;/div&gt;";snapshot测试不通过的原因有两个。一个原因是组件发生了未曾预见的变化,此时应检查代码。另一个原因是组件更新而快照文件并没有更新,此时要运行jest -u更新快照。› 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with -u to update them.结合Vue进行snapshot测试生成快照时需要渲染并挂载组件,在Vue中可以使用官方的单元测试实用工具Vue Test Utils。Vue Test Utils 提供了mount、shallowMount这两个方法,用于创建一个包含被挂载和渲染的 Vue 组件的 Wrapper。component是一个vue组件,options是实例化Vue时的配置,包括挂载选项和其他选项(非挂载选项,会将它们通过extend覆写到其组件选项),结果返回一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法的Wrapper实例。mount(component:{Component}, options:{Object})shallowMount与mount不同的是被存根的子组件,详细请戳文档。Wrapper上的丰富的属性和方法,足以应付本文中的测试需求。html()方法返回Wrapper DOM 节点的 HTML 字符串。find()和findAll()可以查找Wrapper里的DOM节点或Vue组件,可用于查找监听事件的元素。trigger可以在DOM节点/组件上触发一个事件。结合上述的方法,我们可以完成一个模拟事件触发的快照测试。细心的读者可能会发现,我们平时在使用Vue时,数据更新后视图并不会立即更新,需要在nextTick回调中处理更新完成后的任务。但在 Vue Test Utils 中,为简化用法,更新是同步的,所以无需在测试中使用 Vue.nextTick 来等待 DOM 更新。demo演示Vue Test Utils官方文档中提供了一个集成VTU和Jest的demo,不过这个demo比较旧,官方推荐用CLI3创建项目。执行vue create vue-snapshot-demo创建demo项目,创建时要选择单元测试,提供的库有Mocha + Chai及Jest,在这里选择Jest.安装完成之后运行npm run serve即可运行项目。本文中将用一个简单的Todo应用项目来演示。这个Todo应用有简单的添加、删除和修改Todo项状态的功能;Todo项的状态有已完成和未完成,已完成时不可删除,未完成时可删除;已完成的Todo项会用一条线横贯文本,未完成项会在鼠标悬浮时展示删除按钮。组件简单地划分为Todo和TodoItem。TodoItem在Todo项未完成且触发mouseover事件时会展示删除按钮,触发mouseleave时则隐藏按钮(这样可以在快照测试中模拟事件)。TodoItem中有一个checkbox,用于切换Todo项的状态。Todo项完成时会有一个todo-finished类,用于实现删除线效果。为方便这里只介绍TodoItem组件的代码和测试。<template> <li :class="[’todo-item’, item.finished?’todo-finished’:’’]" @mouseover=“handleItemMouseIn” @mouseleave=“handleItemMouseLeave” > <input type=“checkbox” v-model=“item.finished”> <span class=“content”>{{item.content}}</span> <button class=“del-btn” v-show="!item.finished&&hover" @click=“emitDelete”>delete</button> </li></template><script>export default { name: “TodoItem”, props: { item: Object }, data() { return { hover: false }; }, methods: { handleItemMouseIn() { this.hover = true; }, handleItemMouseLeave() { this.hover = false; }, emitDelete() { this.$emit(“delete”); } }};</script><style lang=“scss”>.todo-item { list-style: none; padding: 4px 16px; height: 22px; line-height: 22px; .content { margin-left: 16px; } .del-btn { margin-left: 16px; } &.todo-finished { text-decoration: line-through; }}</style>进行快照测试时,除了测试数据渲染是否正确外还可以模拟事件。这里只贴快照测试用例的代码,完整的代码戳我。describe(‘TodoItem snapshot test’, () => { it(‘first render’, () => { const wrapper = shallowMount(TodoItem, { propsData: { item: { finished: true, content: ’test TodoItem’ } } }) expect(wrapper.html()).toMatchSnapshot() }) it(’toggle checked’, () => { const renderer = createRenderer(); const wrapper = shallowMount(TodoItem, { propsData: { item: { finished: true, content: ’test TodoItem’ } } }) const checkbox = wrapper.find(‘input’); checkbox.trigger(‘click’); renderer.renderToString(wrapper.vm, (err, str) => { expect(str).toMatchSnapshot() }) }) it(‘mouseover’, () => { const renderer = createRenderer(); const wrapper = shallowMount(TodoItem, { propsData: { item: { finished: false, content: ’test TodoItem’ } } }) wrapper.trigger(‘mouseover’); renderer.renderToString(wrapper.vm, (err, str) => { expect(str).toMatchSnapshot() }) })})这里有三个测试。第二个测试模拟checkbox点击,将Todo项从已完成切换到未完成,期待类todo-finished会被移除。第三个测试在未完成Todo项上模拟鼠标悬浮,触发mouseover事件,期待删除按钮会展示。这里使用toMatchSnapshot()来进行匹配快照。这里生成快照文件所需的HTML字符串有wrapper.html()和Renderer.renderToString这两种方式,区别在于前者是同步获取,后者是异步获取。测试模拟事件时,最好以异步方式获取HTML字符串。同步方式获取的字符串并不一定是UI更新后的视图。尽管VTU文档中说所有的更新都是同步,但实际上在第二个快照测试中,如果使用expect(wrapper.html()).toMatchSnapshot(),生成的快照文件中Todo项仍有类todo-finished,期待的结果应该是没有类todo-finished,结果并非更新后的视图。而在第三个快照测试中,使用expect(wrapper.html()).toMatchSnapshot()生成的快照,按钮如期望展示,是UI更新后的视图。所以才不建议在DOM更新的情况下使用wrapper.html()获取HTML字符串。下面是两种对比的结果,1是使用wrapper.html()生成的快照,2是使用Renderer.renderToString生成的。exports[TodoItem snapshot test mouseover 1] = &lt;li class="todo-item"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style=""&gt;delete&lt;/button&gt;&lt;/li&gt;;exports[TodoItem snapshot test mouseover 2] = &lt;li class="todo-item"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn"&gt;delete&lt;/button&gt;&lt;/li&gt;;exports[TodoItem snapshot test toggle checked 1] = &lt;li class="todo-item todo-finished"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style="display: none;"&gt;delete&lt;/button&gt;&lt;/li&gt;;exports[TodoItem snapshot test toggle checked 2] = &lt;li class="todo-item"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style="display:none;"&gt;delete&lt;/button&gt;&lt;/li&gt;;这里使用vue-server-renderer提供的createRenderer来生成一个Renderer实例,实例方法renderToString来获取HTML字符串。这种是典型的回调风格,断言语句在回调中执行即可。 // … wrapper.trigger(‘mouseover’); renderer.renderToString(wrapper.vm, (err, str) => { expect(str).toMatchSnapshot() })如果不想使用这个库,也可以使用VTU中提供的异步案例。由于wrapper.html()是同步获取,所以获取操作及断言语句需要在Vue.nextTick()返回的Promise中执行。 // … wrapper.trigger(‘mouseover’); Vue.nextTick().then(()=>{ expect(wrapper.html()).toMatchSnapshot() })观察测试结果执行npm run test:unit或yarn test:unit运行测试。初次执行,终端输出会有Snapshots: 3 written, 3 total这一行,表示新增三个快照测试,并生成初始快照文件。 › 3 snapshots written.Snapshot Summary › 3 snapshots written from 1 test suite.Test Suites: 1 passed, 1 totalTests: 7 passed, 7 totalSnapshots: 3 written, 3 totalTime: 2.012sRan all test suites.Done in 3.13s.快照文件如下示:// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`TodoItem snapshot test first render 1] = <li class=“todo-item todo-finished”><input type=“checkbox”> <span class=“content”>test TodoItem</span> <button class=“del-btn” style=“display: none;">delete</button></li>;exports[TodoItem snapshot test mouseover 1] = <li class=“todo-item”><input type=“checkbox”> <span class=“content”>test TodoItem</span> <button class=“del-btn”>delete</button></li>;exports[TodoItem snapshot test toggle checked 1] = <li class=“todo-item”><input type=“checkbox”> <span class=“content”>test TodoItem</span> <button class=“del-btn” style=“display:none;">delete</button></li>;第二次执行测试后,输出中有Snapshots: 3 passed, 3 total,表示有三个快照测试成功通过,总共有三个快照测试。Test Suites: 1 passed, 1 totalTests: 7 passed, 7 totalSnapshots: 3 passed, 3 totalTime: 2sRan all test suites.Done in 3.11s.修改第一个快照中传入的content,重新运行测试时,终端会输出不匹配的地方,输出数据的格式与Git类似,会标明哪一行是新增的,哪一行是被删除的,并提示不匹配代码所在行。 - Snapshot + Received - &lt;li class="todo-item todo-finished"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style="display: none;"&gt;delete&lt;/button&gt;&lt;/li&gt; + &lt;li class="todo-item todo-finished"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem content change&lt;/span&gt; &lt;button class="del-btn" style="display: none;"&gt;delete&lt;/button&gt;&lt;/li&gt; 88 | } 89 | }) &gt; 90 | expect(wrapper.html()).toMatchSnapshot() | ^ 91 | }) 92 | 93 | it('toggle checked', () =&gt; { at Object.toMatchSnapshot (tests/unit/TodoItem.spec.js:90:32)同时会提醒你检查代码是否错误或重新运行测试并提供参数-u以更新快照文件。Snapshot Summary › 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with -u` to update them.执行npm run test:unit – -u或yarn test:unit -u更新快照,输出如下示,可以发现有一个快照测试的输出更新了。下次快照测试对照的文件是这个更新后的文件。Test Suites: 1 passed, 1 totalTests: 7 passed, 7 totalSnapshots: 1 updated, 2 passed, 3 totalTime: 2.104s, estimated 3sRan all test suites.Done in 2.93s.其他除了使用toMatchSnapshot()外,还可以使用toMatchInlineSnapshot()。二者不同之处在于toMatchSnapshot()从快照文件中查找快照,而toMatchInlineSnapshot()则将传入的参数当成快照文件进行匹配。配置JestJest配置可以保存在jest.config.js文件里,可以保存在package.json里,用键名jest表示,同时也允许行内配置。介绍几个常用的配置。rootDir查找Jest配置的目录,默认是pwd。testMatchjest查找测试文件的匹配规则,默认是[ “/tests//.js?(x)”, “**/?(.)+(spec|test).js?(x)” ]。默认查找在__test__文件夹中的js/jsx文件和以.test/.spec结尾的js/jsx文件,同时包括test.js和spec.js。snapshotSerializers生成的快照文件中HTML文本没有换行,是否能进行换行美化呢?答案是肯定的。可以在配置中添加snapshotSerializers,接受一个数组,可以对匹配的快照文件做处理。jest-serializer-vue这个库做的就是这样任务。如果你想要实现这个自己的序列化任务,需要实现的方法有test和print。test用于筛选处理的快照,print返回处理后的结果。后记在未了解测试之前,我一直以为测试是枯燥无聊的。了解过快照测试后,我发现测试其实蛮有趣且实用,同时由衷地感叹快照测试的巧妙之处。如果这个简单的案例能让你了解快照测试的作用及使用方法,就是我最大的收获。如果有问题或错误之处,欢迎指出交流。参考链接vue-test-utils-jest-exampleJest - Snapshot TestingVue Test UtilsVue SSR 指南 ...

December 29, 2018 · 3 min · jiezi