frontend9 / fe9-library

九部知识库
1.94k stars 138 forks source link

如果你想更好的测试你的react, 那你可能还需要了解一下这些 #266

Open yanlele opened 5 years ago

yanlele commented 5 years ago

ts测试jest

常见做法

第一步安装包: yarn add -D typescript jest ts-jest @types/jest

创建jest.config.js:

module.exports = {
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
}

base.spec.ts:

import { sampleFunction } from "../src";

test('adds 1 + 2 to equal 3', () => {
    expect(sampleFunction("hello")).toBe("hellohello");
});

关于sessionStorage和localStorage的测试

在jest的最新版本中, 其实已经通过jsdom模拟了 sessionStorage和localStorage 的实现
然后测试的时候, 需要做异步存储, 要不然是不能获取到 存入的值。
例如有这样一个方法:

const sessionStore = {
  get(k) {
    let ret = window.sessionStorage.getItem(k);
    try {
      ret = JSON.parse(ret);
    } catch (e) { console.log(e); }
    return ret;
  },
  set(k, val) {
    let value = val;
    try {
      value = JSON.stringify(val);
    } catch (e) { console.log(e); }
    window.sessionStorage.setItem(k, value);
  },
};

我们写测试用例的时候, 可以这样写:

  describe('sessionStore', () => {
    it('set 方法', async (done) => {
      await expect(sessionStore.set('name', 'yanle'));
      done();
    });
    it('get 方法', (done) => {
      done();
      expect(sessionStore.get('name')).toBe('yanle');
    });
  });

组件测试测试中忽略样式加载和文件加载

在配置jest.config.js中加上这两句

  moduleNameMapper: {
    '^.+\\.(css|less)$': '<rootDir>/test/cssTransform.js',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/test/fileTransform.js',
  },

然后对应文件分别为: cssTransform.js:

module.exports = {};

fileTransform.js:

module.exports = 'test-file-stub';

测试中关于antd组件报undefined的问题

这个情况只会出现在 生成覆盖文件时候会产生
1

这个问题不是个例, antd issue 里面有很多这种情况的讨论。

解决办法:
1、在.babelrc 文件里面 删除import-plugin 配置

{
  "passPerPreset": true,
  "plugins": [
    "relay",
    "transform-runtime",       
    **["import", {"libraryName": "antd", "style": "css"}]**
  ],
  "presets": [
    "react",                   
    "es2015",                  
    "stage-0"
  ]
} 

2、关闭webpack react生产模式核心压缩功能即不会报错
remove these:

new webpack.DefinePlugin({
    "process.env": {
    NODE_ENV: JSON.stringify("production")
    }
});

3、获取组件不要用解构 ReferenceError: Layout is not defined

import { Menu } from 'antd';
const { Item } = Menu;

While:All fine...

import { Menu } from 'antd';
const Item = Menu.Item;

Could not find "store" in either the context or props of "Connect(XXX)"

const mockStore = configureMockStore(); const store = mockStore({});

describe("Testpage Component", () => { it("should render without throwing an error", () => { expect( shallow(

        ).exists(<h1>Test page</h1>)
    ).toBe(true);
});

});


- [Nested components testing with Enzyme inside of React & Redux](https://stackoverflow.com/questions/37798741/nested-components-testing-with-enzyme-inside-of-react-redux)
这个文章里面给出了一个非常好的解决方案                 
Enzyme's mount takes optional parameters. The two that are necessary for what you need are                      
options.context: (Object [optional]): Context to be passed into the component                       
options.childContextTypes: (Object [optional]): Merged contextTypes for all children of the wrapper                 
You would mount SampleComponent with an options object like so:
```js
const store = { 
  subscribe: () => {},
  dispatch: () => {},
  getState: () => ({ ... whatever state you need to pass in ... })
}
const options = {
  context: { store }, 
  childContextTypes: { store: React.PropTypes.object.isRequired } 
}
const _wrapper = mount(<SampleComponent {...defaultProps} />, options)

这种 方式存在的问题是 实际上是伪造了一个context.store, 这个store的功能是不足以让程序去获取真正redux 里面的任何数据, 也无法存储任何数据。
所以不建议用这种方法

Testing with Jest and Webpack aliases

配置方式如下, 直接在jest.config.js 中添加如下配置就可以了:

  "jest": {
    "modulePaths": ["src"],
    "moduleDirectories": ["node_modules"],
    "moduleNameMapper": {
      "^@shared$": "<rootDir>/shared/",
      "^@components$": "<rootDir>/shared/components/"
    }
  },

如何略过redux

下面是来自于stack overflow 的解决方案

and then import that in the test file using the deconstructed assignment syntax:

import { App } from './App'

You can assume (hopefully... ;) ) that Redux and the React bindings have been properly tested by their creators, and spend your time on testing your own code instead.

关于一个测试报错的问题

直接上码说话
组件如下 JestAppTest.jsx

import React, { PureComponent } from 'react';
import { connect } from 'dva';

@connect()
class JestAppTest extends PureComponent {
  render() {
    return (
      <div>
        <p className="name">yanle</p>
        <p className="name2">yanlele</p>
      </div>
    );
  }
}

export default JestAppTest;

测试如下 index.test.js

import React from 'react';
import { shallow } from 'enzyme';
import PropTypes from 'prop-types';
import JestAppTest from './JestAppTest';

const store = {
  subscribe: () => {
  },
  dispatch: () => {
  },
  getState: () => ({}),
};
const options = {
  context: { store },
  childContextTypes: { store: PropTypes.object.isRequired },
};

const indexComponent = shallow(<JestAppTest />, options);
describe('User', () => {
  it('有title', (done) => {
    expect(indexComponent.find('p').at(0).text()).toBe('yanle');
    done();
  });
});

执行命令, 总是会报错

Method “text” is meant to be run on 1 node. 0 found instead.

  19 | describe('User', () => {
  20 |   it('有title', (done) => {
> 21 |     expect(indexComponent.find('p').at(0).text()).toBe('yanle');
     |                                           ^
  22 |     done();
  23 |   });
  24 | });

但是我这里明显就有这个p 标签, 然后却说找不到p标签

原因: 这个地方最重大的原因是因为 connect 本身也是一个高阶组件, 改变了上下文, 渲染方式 shallow和mount 的区别
shallow只渲染当前组件,只能能对当前组件做断言; mount会渲染当前组件以及所有子组件,对所有子组件也可以做上述操作。 一般交互测试都会关心到子组件,我使用的都是mount。但是mount耗时更长,内存啥的也都占用的更多, 如果没必要操作和断言子组件,可以使用shallow。

所以上诉情况下, 直接把 shallow 渲染改为 mount 渲染即可。

dva单元测试

如果项目是用的dva做的模块管理如何解决store注入

存在的问题:我把dva作为独立模块加入到自己的项目里面做状态管理。 用enzyme+jest给react 做测试的时候, 如何处理 Invariant Violation: Could not find "store" in either the context or props of "Connect...." 的问题?
error console.log

Invariant Violation: Could not find "store" in either the context or props of "Connect(UserManagement)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(UserManagement)".

      21 | const IndexComponent = app.start();
      22 | 
    > 23 | const indexComponent = mount(<Index />);
         |                        ^
      24 | describe('User', () => {
      25 |   it('title', (done) => {
      26 |     expect(indexComponent

test code:

import React from 'react';
import { mount } from 'enzyme';
import Index from '../index';

const indexComponent = mount(<Index />);
describe('User', () => {
  it('title', (done) => {
    expect(indexComponent
      .find('p')
      .at(0)
      .text()).toBe('yanle');
    done();
  });
});

这个问题的终极解决办法:
dva.setup.js

import dva from 'dva';
import browserHistory from 'history/createBrowserHistory';
import { routerRedux } from 'dva/router';
import createLoading from 'dva-loading';
import PropTypes from 'prop-types';
import globalErrorHandler from '../globalHandler';
import models from '../models';

const app = dva({
  history: browserHistory(),
  onError: globalErrorHandler,
  extraReducers: {
    router: routerRedux.routerReducer,
  },
});
app.use(createLoading());
models.forEach(m => app.model(m));
app.router(() => ({}));
app.start();

const options = {
  context: { store: app._store },
  childContextTypes: { store: PropTypes.object.isRequired },
};

export default options;

测试代码:

import React from 'react';
import { mount } from 'enzyme';
import Index from '../index';
import options from '../test/dva.setup';

const wrapper = mount(<Index />, options);

describe('User', () => {
  it('title', (done) => {
    expect(wrapper
      .find('.layout_header')
      .at(0)
      .text()).toBe('新建任务');
    done();
  });

  it('has 4 ant-menu-item', () => {
    expect(wrapper.find('.ant-menu-item').length).toBe(4);
  });

  it('table count', async (done) => {
    await expect(wrapper.find('.ant-table-tbody').length).toBe(1);
    done();
  });
});

处理生命周期中的请求

很多请求接口是放置在componentDidMount等生命周期的, 然后通过接口请求, 异步渲染了组件。这个时候就涉及到我们需要模拟这个过程。 否则是拿不到异步渲染的结构的, 通过enzyme也是无法渲染出来。

异步action

异步action测试 可以稍微了解一下这个模块 redux-mock-store

参考文章

YYL1999 commented 5 years ago

不错

paul-3 commented 5 years ago

@yanlele 关于dva做的模块管理如何解决store注入: models, globalErrorHandler是自定义的吗? 刚接触react, dva没多久,不太清楚全局的models怎么写。我现在项目里的model都是单个的。 谢谢🙏

yanlele commented 5 years ago

@paul-3 models 是dva 定义的一个存储模块单元, 符合redux ducks 模块规范定义的一个状态管理单元而已。 建议把 state、reducer/action、effect等集中管理在一个文件里面。 globalErrorHandler 是自定义的。

因为我用的是dva做状态管理, 我了解的dva解决注入store 有三种解决方式: 1、伪造一个虚假的store ;2、有设计到store 的都直接略过; 3、就是从直接从绑定了models 的 dva 示例中 拿到 _store;

上面文章中有这三种方式的一个描述。欢迎多交流。

paul-3 commented 5 years ago

@yanlele 好的,大概了解了。谢谢!

duuliy commented 4 years ago

想用这个终极解决办法真的是困难重重,最后在它app.start('#root'),报错[app.start] container null not found 的时候放弃了、、还是老实的用前面的方法

yearliu commented 3 years ago

@yanlele 请问一下我使用你dva处理方案dva.setup,执行到app.start时报[app.start] extraReducers is conflict with other reducers ,reducers list :router , @@dva,请问这个应该怎么解决呢?

dva commented 3 years ago

Guys, please stop mention me =)

yearliu commented 3 years ago

@dva sorry ,But the error message is displayed like this, : (

yanlele commented 3 years ago

@YearLiu 请问你是还用了其他的 状态管理 模块?

yearliu commented 3 years ago

@yanlele 我们目前框架来说状态管理采用的是dva的那一套,用@connect注入到页面上,方便的话可以看一下你 import models from '../models'; 这个文件是怎么写的吗?

yanlele commented 3 years ago

@yanlele 我们目前框架来说状态管理采用的是dva的那一套,用@connect注入到页面上,方便的话可以看一下你 import models from '../models'; 这个文件是怎么写的吗?

@YearLiu 非常尴尬,已经找不到源码了。 但是这个 models 就是正常的你注册到 dva app model 的那个 models额。

yearliu commented 3 years ago

@yanlele 那请问你对我遇到的这个问题有什么猜想或者头绪吗?我目前还没有成功解决这个问题

Limoer96 commented 3 years ago

@yanlele 你好,我想问下,如果我的models不是单个手动注入而是通过webpack提供require.context批量导入后注入,这种情况下该怎么写测试呢?