Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端单元测试二三事 #27

Open
SunXinFei opened this issue May 20, 2020 · 3 comments
Open

前端单元测试二三事 #27

SunXinFei opened this issue May 20, 2020 · 3 comments

Comments

@SunXinFei
Copy link
Owner

SunXinFei commented May 20, 2020

好处

单元测试的好处就不用多说了,对于敏捷开发的迭代需求或者业务逻辑的重构,有了单元测试之后非常方便的担保业务逻辑平滑过渡,而且单元测试的case的存在,可以有效的说明逻辑,比代码注释更为清晰。在MVVM框架流行的今天,数据驱动DOM使得单元测试更加重要而且可行性更高。

痛点

前端单元测试推动是一直有痛点的,包括一些大厂对前端单元测试这个态度不是很统一。原因主要概括为两点:

  1. 相较于后端单元测试的断言非常清晰,期望的数据通过接口参数不同的调用,而前端除了数据的变化的业务逻辑外,还主要涉及到DOM的操作、变换、展示,随着业务线发展,页面一旦重新变化,case工作白做。
  2. 很多业务部门的业务线生命周期很短,还没写完单元case就已经死掉,转战了业务线,前期更多关注的是业务线的0-1的过程。

从而导致业务线前期稳定性靠开发者把控,后面项目逐渐庞大,测试用例又没有时间补贴,或者开发者转了战场。

折中

比较好的处理方式是折中方案,Utils工具类中的方法必须进行单元测试,业务基础组件和项目的基础业务逻辑必须进行单元测试,这样可以很好的避免后期基础的逻辑,手动痛苦地回归case。而目前看来,前端很多项目也确实是这么做的。其他的case则视重要性与时间代价视情况而定了。

@SunXinFei
Copy link
Owner Author

SunXinFei commented May 20, 2020

Jest在React项目

Jest是FB家的开源的单元测试的工具,对React项目支持非常友好。
jest运行时,如输入值和期望值如果不相符,清晰地标记显示出,方便开发者调试:
image

jest支持localStorage

如果单元测试用例的组件中有localStorage的使用时,会有localstorage is not defined的错误或者调用getItem of null等方法不能使用,
我们建一个setUpTest.js内容如下:

import 'jsdom-global/register';

// browserMocks.js
const localStorageMock = (() => {
  let store = {};

  return {
    getItem(key) {
      return store[key] || null;
    },
    setItem(key, value) {
      store[key] = value.toString();
    },
    clear() {
      store = {};
    },
  };
})();

global.localStorage = localStorageMock;

然后在jest.config.js中配置

setupFilesAfterEnv: ['./tests/setupTests.js'],

jest中的XX_ENV配置

jest.config.js中配置

globals: {
    REACT_APP_ENV: 'dev'
 },

jest支持dva/redux等

这一类的配置有点类似,在业务组件的测试文件如Tag.test.js,注意亮点一个是标签为Tag.WrappedComponent,属性传递的即为Tag组件需要的store中的对象

import { shallow } from 'enzyme';
import React from 'react'; //必须要有,防止jsx在测试环境报错
import Tag from './index';

test('Tag test', () => {
  const wrapper = shallow(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
  expect(wrapper.state('activeKey')).toBe('1');
});

jest测试组件内部方法

import { shallow } from 'enzyme';
import React from 'react'; //必须要有,防止jsx在测试环境报错
import Tag from './index';

test('Tag test', () => {
  const wrapper = shallow(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
  const instance = wrapper.instance();//获取组件的实例
  expect(instance.addFn(2)).toEqual(3);
});

render、mount、shallow的区别

enzyme有3种渲染方式:render、mount、shallow;
render采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。

shallow和mount对组件的渲染结果不是html的dom树,而是react树,如果你chrome装了react devtool插件,他的渲染结果就是react devtool tab下查看的组件结构,而render函数的结果是element tab下查看的结果。

这些只是渲染结果上的差别,更大的差别是shallow和mount的结果是个被封装的ReactWrapper,可以进行多种操作,譬如find()、parents()、children()等选择器进行元素查找;state()、props()进行数据查找,setState()、setprops()操作数据;simulate()模拟事件触发。

shallow只渲染当前组件,只能能对当前组件做断言;mount会渲染当前组件以及所有子组件,对所有子组件也可以做上述操作。一般交互测试都会关心到子组件,使用的都是mount。但是mount耗时更长,内存占用的更多。

jest快照

snapshot是使用render方法,并生成一个文件夹,每次运行进行前后的“快照”对比,这里的“快照”要单独说明一下,不是照片,在不toJSON情况下指的是生成的类似于AST(抽象语法树)的json结构,

import { render } from 'enzyme';
import React from 'react'; //必须要有,防止jsx在测试环境报错
import Tag from './index';

test('Tag test', () => {
  const wrapper = render(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
  expect(wrapper).toMatchSnapshot();
});

jest,enzyme支持jsx中import的css变量与click事件

enzyme默认是支持jsx中css的普通写法的,但是如果组件内部是使用的import的形式,比如:

<div className={styles.add_intersect} onClick={this.addNewGroup}>
       新增组           
</div>

上面这种情况下,我们使用enzyme的find等选择器是获取不到dom元素的,这里我们需要借助
identity-obj-proxy

  1. jest.config.js中添加配置:
 moduleNameMapper: {
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
  },
  1. 测试逻辑为:
import { shallow, mount } from 'enzyme';
test('click action', () => {
  const wrapper = mount(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
  wrapper
    .find('.add_intersect')
    .first()
    .simulate('click');
  expect(wrapper.state('activeObj')).toEqual({
    ext: 'inter',
    index: 1,
  });
});

参考:

jestjs/jest#3094

jest解决window.matchMedia is not a function 或 antdesign中The above error occurred in the <Row> component

setupTests.js中添加下面代码即可,mock出matchMedia:

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

参考

ant-design/ant-design#21096

jest解决alias路径问题

module.exports = {
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1"
  }
}

将这个配置进行添加,这样jest中就可以支持@/utils/utils.js

jest解决Attempted to log "Warning: An update to ForwardRef(TabNavList) inside a test was not wrapped in act(...).``This ensures that you're testing the behavior the user would see in the browser.

setupTests.js中添加如下代码

const mockConsoleMethod = (realConsoleMethod) => {
  const ignoredMessages = [
    'test was not wrapped in act(...)',
  ]

  return (message, ...args) => {
    const containsIgnoredMessage = ignoredMessages.some(ignoredMessage => message.includes(ignoredMessage))

    if (!containsIgnoredMessage) {
      realConsoleMethod(message, ...args)
    }
  }
}

console.warn = jest.fn(mockConsoleMethod(console.warn))
console.error = jest.fn(mockConsoleMethod(console.error))

参考:
enzymejs/enzyme#2073

jest解决puppeteer安装失败导致Browser is not downloaded的问题

jest-puppeteer.config.js中添加如下配置,将运行的browser指向本机的chrome浏览器

launch: {
    executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
  }

@SunXinFei
Copy link
Owner Author

SunXinFei commented Jun 26, 2020

jest在Vue项目

防止element-ui报错找不到:"Unknown custom element: - did you register the component correctly"

  • xxx.spec.js添加element-ui配置
import ElementUI from 'element-ui';
import { shallowMount, createLocalVue } from "@vue/test-utils";
import Rules from "@/views/rules.vue";
import Cases2WashSaveData from "./cases/case-washSaveData.js";

const localVue = createLocalVue()
localVue.use(ElementUI)

test("washSaveData", () => {
  Cases2WashSaveData.forEach((caseData) => {
    const wrapper = shallowMount(Rules,{localVue}, {});
    expect(wrapper.vm.washSaveData(caseData.test)).toStrictEqual(caseData.expect);
  });
});
  • npm i --save-dev identity-obj-proxy 安装

  • jest.config.js添加样式代理

module.exports = {
  preset: "@vue/cli-plugin-unit-jest",
  moduleNameMapper: {
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
  },
};

router-link “Unknown custom element: - did you register the component correctly”

stubs: ['router-link']

 const wrapper = shallowMount(Rules,{localVue, stubs: ['router-link']}, {});

参考: https://stackoverflow.com/questions/49681546/vue-test-utils-unknown-custom-element-router-link

支持element-ui组件的事件测试

  • 将shallowMount替换为mount,使渲染更深层次而不是浅层渲染。

支持vuex

import { shallowMount, createLocalVue, mount } from "@vue/test-utils";
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex)

let actions
let store

beforeEach(() => {
  actions = {
    setMenuExpand: jest.fn()
  }
  store = new Vuex.Store({
    state: {
      menuExpand: false,
    },
    actions
  })
})
/**addNewGroup的click事件 */
test("addNewGroup", () => {
  const wrapper = mount(Rules, { localVue,store, stubs: ["router-link"] });
});

参考:https://vue-test-utils.vuejs.org/zh/guides/#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%AD%E6%B5%8B%E8%AF%95-vuex

相对较全的demo

import ElementUI from "element-ui";
import { shallowMount, createLocalVue, mount } from "@vue/test-utils";
import Rules from "@/views/rules.vue";
import Vuex from "vuex";

const localVue = createLocalVue();
localVue.use(ElementUI);
localVue.use(Vuex);

let actions;
let store;

beforeEach(() => {
  actions = {
    setMenuExpand: jest.fn(),
  };
  store = new Vuex.Store({
    state: {
      menuExpand: false,
    },
    actions,
  });
});

/**addNewGroup的click事件 */
test("addNewGroup", () => {
  const wrapper = mount(Rules, { localVue, store, stubs: ["router-link"] });
  //初始化state数据
  wrapper.setData({
    ruleObj: { include: [[]], exclude: [[]] },
  });
  //寻找Dom
  const button = wrapper.find(".add-group");
  //Dom文本
  expect(button.text()).toBe("Btn");
  //事件触发
  button.trigger("click");
  //state数据
  expect(wrapper.vm.ruleObj).toStrictEqual({
    include: [[], []],
    exclude: [[]],
  });
});

@SunXinFei
Copy link
Owner Author

THREE等webgl工程单元测试

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant