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

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

Open
yanlele opened this issue Apr 3, 2019 · 13 comments
Labels
react react、react-router、react-redux、webpack、mobx

Comments

@yanlele
Copy link
Member

yanlele commented Apr 3, 2019

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)"

import React from "react";
import { shallow } from "enzyme";
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestPage from "../TestPage";

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

describe("Testpage Component", () => {
    it("should render without throwing an error", () => {
        expect(
            shallow(
                <Provider store={store}>
                    <TestPage />
                </Provider>
            ).exists(<h1>Test page</h1>)
        ).toBe(true);
    });
});
  • 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:
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 的解决方案

  • React, Jest, Enzyme how to pass test App.js with store redux
    This is happening because you're trying to test the component without a (which would normally make the store available to its sub-components).
    What I normally do in this situation is test my component without the Redux connect binding. To do this, you can export the App component itself:
export class App extends Component // etc...

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

参考文章

@acodercc acodercc added the react react、react-router、react-redux、webpack、mobx label Apr 8, 2019
@YYL1999
Copy link

YYL1999 commented Apr 9, 2019

不错

@paul-3
Copy link

paul-3 commented Apr 29, 2019

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

@yanlele
Copy link
Member Author

yanlele commented Apr 30, 2019

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

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

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

@paul-3
Copy link

paul-3 commented Apr 30, 2019

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

@duuliy
Copy link

duuliy commented Oct 24, 2019

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

@yearliu
Copy link

yearliu commented Nov 12, 2020

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

@dva
Copy link

dva commented Nov 12, 2020

Guys, please stop mention me =)

@yearliu
Copy link

yearliu commented Nov 13, 2020

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

@yanlele
Copy link
Member Author

yanlele commented Nov 13, 2020

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

@yearliu
Copy link

yearliu commented Nov 13, 2020

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

@yanlele
Copy link
Member Author

yanlele commented Nov 13, 2020

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

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

@yearliu
Copy link

yearliu commented Nov 13, 2020

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

@Limoer96
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react react、react-router、react-redux、webpack、mobx
Projects
None yet
Development

No branches or pull requests

8 participants