Redux: 无法引用包装在 Provider 中或通过连接 Enzyme 的容器

创建于 2016-03-20  ·  51评论  ·  资料来源: reduxjs/redux

我似乎无法引用包含在<Provider>connect中的任何内容

// test
let component = shallow(<Provider store={store}><ContainerComponent /></Provider>);
component.find('#abc'); // returns null

let component = shallow(<Provider store={store}><div id="abc"></div></Provider>);
component.find('#abc'); // returns the div node

// ContainerComponent
const Component = ({...}) => (<div id="abc"></div>);
export default connect(..., ...)(Component);

我遵循: https ://github.com/reactjs/redux/issues/1481 到使用酶编写测试的示例,但是,容器从未在这些示例中进行过测试。 所以我不知道我是否应该/可以测试智能容器?

@fshowalter 有什么想法吗?

最有用的评论

即使它与 Redux 没有直接关系。 我通过再次在容器上调用shallow()解决了这个问题。 它使用传入的存储状态渲染内部组件。

例子:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import events from 'events';

const mockStore = configureMockStore([ thunk ]);
const storeStateMock = {
  myReducer:{
    someState: 'ABC'
  }
};

let store;
let component;
describe('tests for MyContainerComponent', () => {
  beforeEach(() => {
    store = mockStore(storeStateMock);
    component = shallow(<MyContainerComponent store={store} />).shallow();
  });

  it('renders container', () => {
    expect(component.find('div.somediv')).to.have.length.of(1);
  });
  ...
});

希望这可以帮助

所有51条评论

connect()Provider都不是这个库的一部分。 将此类问题提交到适当的存储库时,更容易讨论和跟踪这些问题: https://github.com/reactjs/react-redux。

我认为这实际上是有道理的。 由于您是浅层渲染,因此只会渲染 Provider 组件 - 您的 ContainerComponent 将仅保留其对象输出形式或浅层渲染产生的任何内容。

这里有两个想法:

首先,请注意由connect()生成的包装器组件实际上会在查找props.store之前查找context.store ,因此如果您确实觉得需要测试连接的组件,您应该能够渲染<ConnectedComponent store={myTestStore} />而无需担心 Provider 或 context 或任何东西。

第二个问题是您是否真的需要担心实际测试全连接组件。 我所看到的论点是,如果您可以使用特定的道具测试您的“普通”组件,并且您可以测试您的mapStateToProps实现,您可以放心地假设 react-redux 将它们正确地组合在一起,并且不需要实际测试连接的版本本身。

@gaearon你是对的,对不起。 不知道是在反应还是酶回购中提出这个问题。

@markerikson测试智能组件的原因是为了mapToDispatchProps我想确保正确的调度程序被包装的组件调用。 只是将 store 传递到ConnectedComponent意味着我不会测试状态映射或调度回调函数。

如果需要,我会更多地考虑这一点,并在相关回购中提出问题。 谢谢您的帮助。

@mordra :当您说“调用了正确的调度员”时,您实际上是指“正确的动作创建者”吗?

你可以做这样的事情(语法可能是关闭的,但你应该明白):

let actionSpy = sinon.spy();

let wrapper = shallow(<PlainComponent someActionProp={actionSpy} />);
wrapper.find(".triggerActionButton").simulate("click");
expect(actionSpy.calledOnce).to.be.true;
expect(actionSpy.calledWith({type : ACTION_I_WAS_EXPECTING)).to.be.true;

换句话说,渲染你的普通组件,传递从mapDispatch返回的动作的道具的间谍,并触发组件需要使其调用这些动作的任何行为。

另外,根据我之前的评论:您应该能够分别测试您的mapStateToPropsmapDispatchToProps ,并且感到安全的是 React-Redux 会适当地调用它们,而不必尝试测试连接的版本自己来验证情况是否如此。

@markerikson我不能按照你的建议去做,因为我的PlainComponent不知道动作或动作创建者。 我不知道这是否是正确的方法,但如果你看:
https://github.com/mordra/cotwmtor/blob/master/client/charCreation/charCreation.jsx
您会看到我的mapDispatchToProps包含所有逻辑:

const mapDispatchToProps = (dispatch) => {
  return {
    onCompleted      : (player) => {
      Meteor.call('newGame', player, function (data) {
        console.log('new game return: ' + data);
      });
      dispatch(routeActions.push('/game'));
      dispatch({type: "INIT_GAME", map: generateAreas(), buildings: generateBuildings(dispatch)});
    },
    onCancelled      : () => dispatch(routeActions.push('/')),
    onChangeName     : input => dispatch(actions.changeName(input)),
    onChangeGender   : gender => dispatch(actions.setGender(gender)),
    onSetDifficulty  : lvl => dispatch(actions.setDifficulty(lvl)),
    onChangeAttribute: (attr, val) => dispatch(actions.setAttribute(attr, val))
  }
};

因此,如果我监视传递给 PlainComponent 的 props,我将无法测试是否调用了正确的操作,而只是传递给操作创建者的参数是正确的。
然后我需要单独测试connect组件。

啊。 这有助于我理解更多。

所以需要注意的是,我实际上几乎没有编写测试的实际经验,有几个想法:

  • 您实际上拥有的是动作创建者,它们只是没有单独定义,因为您想要访问dispatch 。 这本身并不是很容易测试的。 您可能还希望能够测试它们的行为,并且要做到这一点,您需要将它们定义为mapDispatch之外的它们自己的函数。
  • 这些看起来都非常适合与 redux-thunk 一起使用,这肯定会更容易自己定义这些函数并允许它们访问dispatch
  • 您的 PlainComponent _does_ 知道“动作”,或者在这种情况下至少知道回调,因为您正在将它们传递进去,并且在您的组件中的某处您正在运行this.props.onSetDifficulty("HARD")或类似的东西。 当我建议通过间谍行动时,这就是我建议替换的那种东西。 因此,如果您为onSetDifficulty传递了一个间谍,您可以验证您的组件是否调用了它,并为难度级别传递了一个可接受的值。

最终,我认为您应该能够通过采用mapDispatch中定义的那些函数并分别定义它们来使事情更具可测试性。 然后,您不必担心必须连接mapDispatch以正确测试您的组件,并且可以专注于组件是否刚刚使用正确的参数或其他东西调用了给定的 prop 回调。

另外,从组件的角度来看:最终它实际上并不担心{action: CHANGE_NAME, name : "Fred"}是否被调度。 它所知道的是它调用this.props.onChangeName("Fred") ,而 _that_ 是你应该尝试测试的——它以正确的方式调用了 prop 回调。

@mordra在你的情况下,我会导出mapDispatchToProps并单独测试它。 您可以验证属性名称是否正确,并传递一个间谍进行调度以测试您的动作创建者。

也就是说,我倾向于避免使用mapDispatchToProps来支持mapActionCreatorsToProps ,它映射了我可以轻松单独测试的已经形成的动作创建者。 在这种情况下,我只是测试我的所有道具都被定义为防止导入拼写错误。

最后请注意,您可以使用普通(非浅层)渲染。 为此,您需要 jsdom 或真正的浏览器,但是您可以渲染任何深度。

不知道是在反应还是酶回购中提出这个问题

抱歉,我不是指 React 或 Enzyme 存储库。 我的意思是你正在使用的库React Redux

感谢大家的帮助,这对我来说有很多信息要消化。 @markerikson @fshowalter我做了更多的环顾四周,并会接受您导出 mapState/Dispatch 并单独测试它们的建议,测试唤起预期动作的回调是有意义的。

@gaearon无状态组件不会在没有浅层的情况下渲染,只是略过问题,渲染具有嵌套无状态组件的有状态组件似乎存在问题,所以我已经做了一个心理笔记,以暂时避免这条路径。

我不确定你指的是哪些问题。 您可以很好地使用功能组件的浅层渲染。 浅层渲染只能工作一层(不管组件是如何定义的)。 这是它的主要特点——它不递归,因此组件测试保持独立。 如果您有可以重现的浅层渲染的特定问题,请针对 React 存储库提交错误。 谢谢!

@gaearon :所以澄清一下,如果我这样做:

let wrapper = shallow(<A><B /></A>);

B 是否会被渲染? 因为我认为这就是最初的问题 - 尝试在连接的组件周围呈现<Provider>以测试连接。

即使它与 Redux 没有直接关系。 我通过再次在容器上调用shallow()解决了这个问题。 它使用传入的存储状态渲染内部组件。

例子:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import events from 'events';

const mockStore = configureMockStore([ thunk ]);
const storeStateMock = {
  myReducer:{
    someState: 'ABC'
  }
};

let store;
let component;
describe('tests for MyContainerComponent', () => {
  beforeEach(() => {
    store = mockStore(storeStateMock);
    component = shallow(<MyContainerComponent store={store} />).shallow();
  });

  it('renders container', () => {
    expect(component.find('div.somediv')).to.have.length.of(1);
  });
  ...
});

希望这可以帮助

我们是否应该通过自定义模块采取更平易近人的方式来做到这一点?

编辑:所以我们可以将连接声明为酶的一种方法,例如component = shallowWithConnect()

@ev1stensberg这是个好主意。 我们是否已经决定这是否是方法和/或是否已经开始着手这方面的工作? 如果没有,我很乐意做出贡献。

对于将来偶然发现此问题的任何人,这是对我有用的方法:只需自行测试普通组件即可。 将组件定义导出为命名导出,并将连接的组件(用于您的应用程序)导出为默认导出。 回到原始问题的代码:

// test
let component = shallow(<Provider store={store}><ContainerComponent /></Provider>);
component.find('#abc'); // returns null

let component = shallow(<Provider store={store}><div id="abc"></div></Provider>);
component.find('#abc'); // returns the div node

// ContainerComponent
export const Component = ({...}) => (<div id="abc"></div>); // I EXPORT THIS, TOO, JUST FOR TESTING
export default connect(..., ...)(Component);

然后就做

const component = shallow(<Component {...props} />)
// test component

正如其他人在上面指出的那样,如果您还覆盖了传递给 connect 的功能,那么您应该全面覆盖整个组件。

嗯,首先要注意的是

  1. 将状态从商店传达给
  2. 通过调度动作来修改状态事件。

如果我没问题,那么您只需要测试 2 件事:

  1. 您的组件是否接收到从状态生成的正确道具(可以通过选择器)?
  2. 您的组件是否调度了正确的操作?

所以这是我的方法

import React from 'react';
import { shallow } from 'enzyme';
import { fromJS } from 'immutable';
import { createStore } from 'redux';
// this is the <Map /> container
import Map from '../index';
// this is an action handled by a reducer
import { mSetRegion } from '../reducer';
// this is an action handled by a saga
import { sNewChan } from '../sagas';
// this is the component wrapped by the <Map /> container
import MapComponent from '../../../components/Map';

const region = {
  latitude: 20.1449858,
  longitude: -16.8884463,
  latitudeDelta: 0.0222,
  longitudeDelta: 0.0321,
};
const otherRegion = {
  latitude: 21.1449858,
  longitude: -12.8884463,
  latitudeDelta: 1.0222,
  longitudeDelta: 2.0321,
};
const coordinate = {
  latitude: 31.788,
  longitude: -102.43,
};
const marker1 = {
  coordinate,
};
const markers = [marker1];
const mapState = fromJS({
  region,
  markers,
});
const initialState = {
  map: mapState,
};
// we are not testing the reducer, so it's ok to have a do-nothing reducer
const reducer = state => state;
// just a mock
const dispatch = jest.fn();
// this is how i mock the dispatch method
const store = {
  ...createStore(reducer, initialState),
  dispatch,
};
const wrapper = shallow(
  <Map store={store} />,
);
const component = wrapper.find(MapComponent);

describe('<Map />', () => {
  it('should render', () => {
    expect(wrapper).toBeTruthy();
  });

  it('should pass the region as prop', () => {
    expect(component.props().region).toEqual(region);
  });

  it('should pass the markers as prop', () => {
    expect(component.props().markers).toEqual(markers);
  });
  // a signal is an action wich will be handled by a saga
  it('should dispatch an newChan signal', () => {
    component.simulate('longPress', { nativeEvent: { coordinate } });
    expect(dispatch.mock.calls.length).toBe(1);
    expect(dispatch.mock.calls[0][0]).toEqual(sNewChan(coordinate));
  });
  // a message is an action wich will be handled by a reducer
  it('should dispatch a setRegion message', () => {
    component.simulate('regionChange', otherRegion);
    expect(dispatch.mock.calls.length).toBe(2);
    expect(dispatch.mock.calls[1][0]).toEqual(mSetRegion(otherRegion));
  });
});

@markerikson你是什么意思:

你可以测试你的 mapStateToProps 实现

假设您正在通过 ES6 导出导出连接的容器。 您将无权访问私有 mapStateToProps。 您是在谈论其他方式还是可以具体一点?

@mordrax

您可以验证属性名称是否正确

因此,如果您公开您的 mapStateToProps 并将其公开,然后假设您从测试中渲染您的 ContainerComponent,包括发送假商店和发送状态道具,那么您的 mapStateToProps 接收该状态,将其映射到道具。 但是你怎么能从那时起测试它呢? Connect 将调用 mapStateToProps 从而合并道具,但是在该过程/流程中,接缝在哪里以及代码中的哪个点是您可以拦截在演示组件上设置的道具的地方? 我想像@guatedude2这样的双重浅化可能是一种检查映射道具而没有任何接缝的方法......

也在你的 shallow.shallow 上。 那么 connect() 组件将什么返回,一个包装器,对吗? 那个包装器是什么包装了展示组件?

@granmoe您能否更详细地解释一下您的确切含义:

如果您还覆盖了传递给 connect 的功能,那么您应该全面覆盖整个组件。

在我想要测试的内容和原因方面,我也支持@tugorez

@markerikson间谍的好例子。 这是你可以测试的一件事,但你需要更多的断言来测试“行为单元”。 仅仅测试从 mapDispatchToProps 调用了一个间谍操作并不能真正告诉我们有关容器组件的整个故事。 它不会根据容器的处理程序逻辑断言预期的结果。

仅仅测试你是否传递了 props 或 state 是不够的,你还想测试容器组件的行为。 这意味着测试两件事:

1)容器组件是否将道具连接到演示组件,如果是,是否存在某种状态,我希望演示组件在基于道具传递给演示组件的特定状态呈现后具有某种状态。 谁知道呢,也许一些愚蠢的开发人员会出现并向 mapStateToProps 添加更多代码,您不能总是相信它会正确映射事物,这就是测试容器逻辑的重点。 虽然在 mapStateToProps 中实际上没有任何逻辑,但谁知道又有一些开发人员出现并在其中广告了一个 if 语句……嗯,这种行为可能会把事情搞砸。

2)调度处理程序逻辑(行为)是否在容器中工作。

@dschinkel

定义 Redux 连接组件的典型做法是export default connect()(MyComponent) ,并将export MyComponent定义为命名导出。 同样,因为mapState只是一个函数,你也可以export const mapState = () => {} ,然后单独导入测试。

至于测试“容器组件”与“展示组件”:这里的一般想法是您可能不需要关心大部分测试容器,其中“容器”===“的输出connect ”。 React-Redux 已经有一个完整的测试套件来说明它的行为方式,你没有理由重复这项工作。 我们_知道_它正确处理了对商店的订阅,调用mapStatemapDispatch ,并将道具传递给包装的组件。

_you_ 作为库的使用者会感兴趣的是 _your_ 组件的行为方式。 实际上,您自己的组件是否从connect包装器、测试或其他东西中获取道具并不重要——这只是在给定一组道具的情况下该组件的行为方式。

(另外,FWIW,我意识到你绝对是测试专家,而我不是,但它_确实_似乎你对事情有点偏执:)如果你担心mapState是意外损坏,然后为它编写测试并继续前进。)

现在,如果您的包装组件然后在其中渲染其他连接的组件,特别是如果您正在执行完整渲染而不是浅层渲染,那么通过执行const wrapper = mount(<Provider store={testStore}><MyConnectedComponent /></Provider>来测试事情可能会更简单。 但是,总的来说,我的看法是,大多数时候你应该能够分别测试你的mapState函数和“普通”组件,而且你真的不应该仅仅为了验证而尝试测试连接的版本mapState的输出被传递给普通组件。

作为参考,您可能想看看 Dan 不久前写的connect的缩影版本,以说明它在内部的作用: https ://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e。

是的,我的意思是如果你导出 mapStateToProps 和 dispatchStateToProps,那会更好。 我不想测试 connect() (我测试中的合作者)是否有效,绝对不是。 所以这将是一种明确的方法,导出这两种方法:

示例容器-spec.js

describe('testing mapPropsToState directly', () => {
    it.only('returns expected state', () => {
        const state = {firstName: 'Dave'};
        const output = mapStateToProps(state);

        expect(output.firstName).to.equal('Dave');
    });
});

容器

import React from 'react';
import { connect } from 'react-redux'
import Example from './Example';

export const mapStateToProps = (state) => ({
    firstName: state.firstName
})

export default connect(mapStateToProps)(Example);

介绍

import React, { Component } from 'react';

export default class Example extends Component {
    render(){
        return (
            <div className='example'>
                {this.props.firstName}
            </div>
        )
    }
}

再说一次,你也可以用 _shallow_ 渲染做这样的事情,并且仍然潜入子组件:

示例容器-spec.js

it('persists firstName to presentation component', () => {
        var state = {firstName: "Dave"};
        const container = mount(<ExampleContainer store={fakeStore(state)}/>),
            example = container.find(Example);

        expect(example.length).to.equal(1);
        expect(example.text()).to.equal(state.firstName);
    });

容器

import React from 'react';
import { connect } from 'react-redux'
import Example from './Example';

const mapStateToProps = (state) => ({
    name: state.firstName
})

export default connect(mapStateToProps)(Example);

介绍

import React, { Component } from 'react';

export default class Example extends Component {
    render(){
        return (
            <div className='example'>
                {this.props.firstName}
            </div>
        )
    }
}

另一种浅显的方法是做双重浅显。 浅化父级上的浅层将使其包装的子组件变浅,如下所示:

it('get child component by double shallow()', () => {
        const container = shallow(<ExampleContainer store={fakeStore({})}/>),
            presentationalChildComponent = container.find(Example).shallow().find('.example');

        expect(presentationalChildComponent).to.have.length(1);
    });

所以这只是另一种方式,您正在检查演示组件的结果。

无论哪种方式都可以......但是就像你说的那样,导出 mapStateToProps 可能是最好的,因为这就是我关心的设置道具状态方面的测试。

我意识到你绝对是测试专家,而我不是,但看起来你对事情有点偏执了:)

不,我不是,测试正确意味着拥有不脆弱且提供价值的良好测试很重要。 首先,没有人是_专家_。 每个人都在不断地学习,有些人只是因为自负而不愿承认这一点。 它被称为软件工艺(又名专业)。

像你说的那样只测试行为而不是测试协作者(connect() 本身)很重要。 第一次弄清楚这一点并进行良好的测试很重要。 确保您不测试协作者很重要,有时需要在其他开发人员之间进行讨论。

您可以进行易碎测试,重要的是不要进行易碎测试。 应该认真对待测试,这意味着不断检查你是否正确地接近它。 我只是想弄清楚我的流程。 我是 TDD,所以我如何开始很重要,而良好的测试也很重要。 因此,当他们试图了解测试 React 组件的不同方法时,任何人都不应该觉得自己是“偏执狂”。

新的 React 测试回购即将到来...
事实上,我很快就会分享一个 repo,它展示了测试 react-redux 的各种风格和组合以及方法。 我认为它会帮助人们,因为我不是唯一考虑这个的人。 你一次又一次地看到人们围绕测试 redux 容器和其他组件提出相同的问题。 Redux 文档和 react-redux 文档都没有做到这一点,他们在 IMO 测试领域缺乏文档。 它只触及表面,我们需要一个很好的 repo 来展示关于接近 React 测试的各种风格,所以我很快就会把它放上来。

If anyone would like to contribute to that repo, please get a hold of me所以我可以给你添加一个合作者。 我希望人们添加示例。 我希望看到直接使用React Test Utils + mocha的示例,使用EnzymeAvaJesttape等。

结论

我认为我们讨论过的两种方法都很好。 直接测试方法,或者像我上面做的那样测试,潜入其中。 有时你不想让你的测试基于特定的方法,因为你可能会得到脆弱的测试......这就是为什么人们在过去被测试刺痛的原因。 因此,是否围绕一个功能进行测试,是否测试“单元”取决于。 有时你不想公开一个方法,最好在上面测试合同并将其中的一些保密。 因此,在编写测试时考虑经常在做什么总是很重要的。 没有错。 编写好的测试并不容易。

这正是 TDD 所提倡的,经常这样做。 这样你就可以得到更精简更好更不脆弱的代码。 TDD 迫使您提前考虑测试,而不是稍后。 这就是不同之处,也是为什么像我这样的人想要了解不同的流程和方法,因为当我进行 TDD 时,它迫使我经常以小块重新评估设计,这意味着经常考虑我的测试方法。

@markerikson感谢您的投入,我喜欢谈论测试。 好东西人。 我并没有质疑您的专业知识,只是我实际上并没有尝试自己直接测试 mapStateToProps! 作为非测试人员,您并没有做坏事;)。 我只是 redux 的新手,就这么简单,所以我会有一些不仅仅是“触及表面”的问题。 我们应该在较低级别讨论测试,以便人们了解他们在做什么,他们如何做到这一点,以及他们是否从他们的方法中受益。 要做到这一点,您必须了解较低级别的连接、react-redux、提供程序......这样您就知道如何“仅测试行为”。 我还没有看到很多人导出 mapStateToProps这对我来说也很有趣……这让我想知道为什么不这样做。

WeDoTDD.com
顺便说一句,对于任何感兴趣的人,我去年创办了WeDoTDD.com (顺便说一句,用 React 编写)。 如果特定团队(特定团队中的所有开发人员)或您的整个公司(有些公司确实拥有所有 TDD 的开发人员,尤其是咨询公司),请通过slack.wedotdd.com与我联系

更新。

在更多地测试连接的组件并思考什么对我有用之后,我现在 100% 同意@markerikson的声明:

如果你真的觉得你需要测试一个连接的组件,你应该能够渲染无需担心提供者或上下文或任何东西。

我没有看到有任何需要在我的测试中引入<Provider /> ,如果你这样做似乎你正在测试一个合作者,而重点不是测试<Provider />是否有效。 您应该测试组件中的行为是否有效,并且您的容器是否通过上下文获取存储并不重要。 只需将商店作为道具传递..connect 只需要某种方式进入商店,它并不关心(上下文或道具)如何......在您的测试中完全摆脱提供者。

我也认为不需要使用酶的上下文选项,这是您可以通过 React 的上下文持久存储存储的另一种方式。 在您的测试中完全忘记 react 的上下文,这完全是不必要的膨胀,并使您的测试更复杂,更难维护和阅读。

通过将 mapStateToProps 纯函数导出到容器文件中来直接测试它。 我正在使用测试 mapStateToProps + 的组合以及对我的容器进行浅层测试的几个测试,验证展示组件至少正在接收我期望的道具,然后我停止在那里进行道具测试。 我还决定不对我的容器组件测试做任何双重浅化。 TDD 给我的反馈是,如果尝试这样做,测试会变得过于复杂,您可能做错了什么。 那个“错误”是“不要为你的容器组件测试加倍浅的东西”。 而是创建一套新的展示组件测试,独立测试展示组件的输出。

很高兴听到你把事情解决了。 (FWIW,我的评论主要是为了回答你关于“接缝”和“挖掘道具”的问题,这似乎真的是在不必要的细节层面上几乎没有任何好处。)

如果您正在对渲染其他连接组件的组件进行完整渲染,那么使用<Provider> /context _is_ 将是必要的,因为这些连接的子节点将在上下文中查找商店。

如果你有改进 Redux 当前测试文档的建议,请务必提交 PR。

@markerikson在山上的好点。 如果我需要测试我正在测试的组件的生命周期,我只会做一个 mount。 我不会用它来深入研究子组件......这将不再符合单元测试的条件,并且基本上是集成测试,当你应该在他们的子组件上测试这些子组件时,你会将该测试耦合到实现细节单独拥有,而不是通过父组件。

总之好东西,谢谢!

对,只是说如果您_do_使用mount来检查相关组件的生命周期,并且该组件呈现其他连接的组件,那么您_将_需要这些嵌套组件在上下文中可用的存储以避免错误被测试的组件被渲染。

(编辑:我显然是在重复自己,抱歉 - 在多个地方回答问题而不总是通过一个线程返回,甚至只是向上滚动一点的危险。)

啊,好的,明白了! 是的,没想到...

感谢这里的所有评论, @markerikson和 @dschinkel! 他们非常有帮助。 我现在决定导出我未连接的组件并像其他任何组件一样对其进行测试,并分别测试我的mapStateToPropsmapDispatchToProps 。 但是,有一种情况是这些测试没有捕捉到的,那就是map(State/Dispatch)ToProps实际上没有传递到对connect的调用中。 例子:

import React from 'react';
import { connect } from 'react-redux';

export const mapStateToProps = ({ name }) => ({ name });

export const Example = ({ name }) => <h1>{name}</h1>;

export default connect({ name } => ({ name: firstName }))(Example); // Whoops, didn't actually pass in `mapStateToProps`.

这在实践中可能永远不会发生,但它可能会发生,特别是因为 linter 不会将mapStateToProps报告为未使用的变量,因为它正在被导出。

@danny-andrews 您能否更具体地了解如何测试您的 mapDispatchToProps 函数?

@leizard

我不再直接测试 mapDispatchToProps 或 mapStateToProps 了。 自从这个帖子以来,我改变了主意,并建议不要这样做。

同时测试它们你会遇到@danny-andrews 问题,但问题更多是关于良好的测试并且试图公开这两个函数不是一个好主意。 测试结束了。 您应该通过测试行为或通过浅容器测试道具来间接测试这两种方法。 我发现没有理由尝试公开这些私有方法,我现在意识到尝试这样做也是一种不好的测试实践。

所以我不同意@markerikson ,通过您连接的组件进行测试。

找到正确的 API/合约来测试而不是过度测试,这是关键宝贝:)。

@dschinkel

您应该通过测试行为来间接测试这两种方法......
所以我不同意@markerikson ,通过您连接的组件进行测试。

您是否建议您应该完全放弃导出未连接的组件而只测试连接的组件? 我认为是“过度测试”。 这不必要地将您的测试与它正在测试的组件的实现细节联系在一起。 因为未连接组件的功能(如果有的话,那就是另外一罐蠕虫)与它从哪里接收道具完全无关。 也许您希望将来将该组件变成纯粹的展示组件。 如果您测试连接的组件,则必须更改所有测试以不再注入商店,而是直接传递道具。 如果你测试了未连接的组件,你不需要做任何事情,除了吹掉连接组件的特定测试(更多内容见下文)。

不过,我同意您不应该直接测试mapStateToProps / mapDispatchToProps 。 仅仅为了测试目的而公开这些私有方法对我来说总是感觉像是一种代码味道。 事实上,我认为这是唯一一次你应该测试连接的组件。 所以,总结一下:

  1. 导出未连接组件和连接组件
  2. 不要导出mapStateToPropsmapDispatchToProps
  3. 通过未连接的组件测试所有组件逻辑
  4. 仅在测试与 redux 的交互时测试连接的组件(数据道具从存储中的适当位置传入/动作道具分配给适当的动作创建者等)。

执行第 4 项的唯一额外开销是您必须删除 redux 存储以将其传递到连接的组件。 不过,这很容易,因为它的 API 只有三种方法。

MapStateToProps 方法

测试容器.jsx:

import { connect } from 'react-redux';

export const TestContainer = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

export const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

export default connect(mapStateToProps)(TestContainer); // Ewww, exposing private methods just for testing

TestContainer.spec.jsx:

import React from 'react';
import { shallow } from 'enzyme';

import TestContainer, { mapStateToProps } from './TestContainer';

it('maps auth.userPermissions to permissions', () => {
  const userPermissions = {};

  const { permissions: actual } = mapStateToProps({
    auth: { userPermissions },
  });

  expect(actual).toBe(userPermissions);
});

新方法

测试容器.jsx:

import { connect } from 'react-redux';

export const TestContainer = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

export default connect(mapStateToProps)(TestContainer);

TestContainer.spec.jsx:

import React from 'react';
import { shallow } from 'enzyme';

import TestContainer, { TestComponent } from './TestContainer';

it('renders TestComponent with approriate props from store', () => {
  const userPermissions = {};
  // Stubbing out store. Would normally use a helper method. Did it inline for simplicity.
  const store = {
    getState: () => ({
      auth: { userPermissions },
    }),
    dispatch: () => {},
    subscribe: () => {},
  };
  const subject = shallow(<TestContainer store={store} />).find(TestComponent);

  const actual = subject.prop('permissions');
  expect(actual).toBe(userPermissions);
});

是的,这就是我所说的我不导出和测试 mapStateToProps ,它已经过测试了。 通过API进行测试。 这里的 API 是Container

@丹尼安德鲁斯

通过未连接的组件测试所有组件逻辑

你能找到一些例子吗?

我的意思和你说的一样:“你应该通过测试行为来间接测试这两种方法”

这可能有助于澄清术语:当我说“未连接的组件”时,我指的是包装在对connect的调用中的原始组件,当我说“连接的组件”时,我指的是从返回的结果组件对connect的调用。 我相信“连接组件”和“容器”是同义词。 从我上面:

// Unconnected component. Test all component logic by importing this guy.
export const TestComponent = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

// Connected component (container). Only test interaction with redux by importing this guy.
export default connect(mapStateToProps)(TestComponent);

有些人宁愿完全拆分这些,让他们的“容器”只是对connect的调用,它包装了一个纯粹的演示组件。 在这种情况下,列表中的第 1 和第 3 项就消失了,您需要编写的唯一测试是验证与 redux 交互的测试(第 4 项)。

我计划为此写一篇巨大的博客文章。 我现在正忙于代码,但稍后会回复

多次阅读此线程后,我得出的结论是推荐了 3 种方法:

  1. 导出mapDispatchToPropsmapStateToProps并分别测试
  2. 浅渲染Container组件并测试接线是否正确
  3. 使用选择器而不是mapStateToProps和动作创建者而不是mapDispatchToProps并分别测试它们; 一起使用 action creators、reducers 和 selectors 编写测试,以确保整个流程正常工作。

最后,我想所有选项都是有效的,但各有利弊。 “最纯粹”的可能是第二个,但也涉及最多的工作。 我个人的偏好实际上会倾向于选项 3。
我在博客上对此进行了更详细的描述,其中也有一些代码示例。

@ucorina
在当前的 ES6 模块世界中,每个文件都被认为是一个封装在其自身范围内的模块。 导出是你的小模块的API,导出方法感觉非常错误,只是为了你可以测试它们。 将私有方法公开的方式与此类似,只是为了测试它们。

这就是为什么我倾向于@dschinkel方法而不是导出 mapDispatch 和 mapState,因为对我来说它们是连接组件的私有实现。

但是我不确定如何处理一个组件,它只将另一个组件包裹在一个连接周围。

你写过博客文章吗? @dschinkel很想读

@AnaRobynn

是的,我同意你的观点,只是导出mapDispatchmapState感觉不对,我自己可能不会使用它,但它是一个有效的选项,即使一个不太“纯”的选项. 我把它留在那里作为一个可能的选择也是因为 Dan Abramov 在这里提出了这种确切的技术: https ://github.com/reduxjs/react-redux/issues/325#issuecomment -199449298。

为了回答您的“如何处理一个组件,它只将另一个组件包裹在一个连接周围”的问题,我想说不要测试它。 没有与您的应用程序相关的业务逻辑,并且您不想测试connect实现 - 这已经在react-redux库中进行了测试。

如果您仍然想测试,为了将来证明您的代码,您始终可以遵循我在此处描述的第二种方法,它更接近于@dschinkel所描述的。

@ucorina没错,我认为选项也是我会选择的选项! 感谢您更深入地解释您的答案。 这是一篇写得很好的博客文章。 恭喜!

@ucorina很抱歉再次打扰,但是在睡了一会儿之后,我也不确定我是否完全同意选项 2。 不过我不确定。 我也不确定测试 mapStateToProps 的好处。

你认为“选项 2”也可以间接测试动作创建者吗? 并且还间接测试选择器? 那是件好事儿吗?
我不是 100% 相信这一点,但我也对那里的所有(错误)信息感到困惑。 测试有很多不同的想法。

  1. 仅在mapStateToProps内使用选择器

    • 如果需要,在容器测试期间模拟它们

    • 单独测试选择器。

    • 仅在mapDispatchToProps内使用动作创建者

    • 如果需要,在容器测试期间模拟它们。

    • 单独测试动作创建者。

    • 浅连接组件

    • 断言它被渲染

    • 测试必须添加到mapStateToPropsmapDispatchToPropsmergeProps并且尚未在其他地方测试过的任何额外逻辑。 (条件调度,从ownProps获取参数的选择器等)



      • 在这种情况下,您只是在测试模拟被调用的正确次数和正确的参数。



选择:

  • 模拟存储状态以一起测试容器+选择器
  • 模拟外部 api 调用以一起测试容器+动作

@AnaRobynn我相信@ucorina的第二种方法是正确的。

我听到的很多信息表明:

  1. 您应该公开 mapDispatchToProps,并直接对其进行测试
  2. 您无需测试对 connect() 的调用,因为 connect() 已经过测试

关于 1,我认为为了测试而暴露内部结构不是一个好习惯。 您的测试 A:现在假设内部结构,而 B:应该测试它们将如何实际使用的东西。

关于 2,您_绝对_应该测试调用外部库的东西。 这是任何破解应用程序的弱点之一。 对于任何库,仍然有可能在次要/补丁版本中引入重大更改,并且您_希望_您的测试快速失败。

你认为“选项 2”也可以间接测试动作创建者吗? 并且还间接测试选择器? 那是件好事儿吗?

是的,冗余测试覆盖是一件好事。

@philihp @dougbacelar
我相信我与测试 React 应用程序有点相关。 我目前对这个主题的信念和想法:

  1. 如果您不需要多个组件中的这些映射,请不要公开mapStateToPropsmapDispatchToProps 。 导出它们只是为了测试是一种反模式。
  2. 我目前将容器视为协作者(委托其他功能和其他功能执行真实逻辑的功能)。
    那是什么意思?
  3. 容器不执行任何逻辑
  4. 选择状态的逻辑用于选择器功能(可以纯粹进行单元测试)
  5. 动作的逻辑隐藏在动作创建者中(这可能取决于您是否使用 redux-thunk 本身是否是协作者)
  6. 渲染的视图是另一个函数(可能有另一个协作函数或纯粹渲染东西

=> 当您使用正确的测试库时,它的测试设置实际上非常简单(我推荐 testdouble.js)。
=> 通过 testdouble.js 模拟你的选择器和操作(使用td.when()
=> 浅渲染您的容器组件, dive()一次以访问视图组件的方法
=> 给定td.when()的规则集,断言组件是否行为正确

不需要注入一些 fakeStore 库,存根存储就可以了。

例子:

/** <strong i="27">@format</strong> */

import React from 'react';
import { shallow } from 'enzyme';

function setup(props) {
  const HasOrganization = require('./HasOrganization').default;
  const defaultProps = {
    store: {
      subscribe: Function.prototype,
      getState: Function.prototype,
      dispatch: Function.prototype,
    },
  };
  const container = shallow(<HasOrganization {...defaultProps} {...props} />);
  return {
    container,
    wrapper: container.dive(),
  };
}

describe('<HasOrganization /> collaborations', () => {
  let getCurrentOrganizationId;
  beforeEach(() => {
    getCurrentOrganizationId = td.replace('../containers/App/userSelectors')
      .selectCurrentOrganizationId;
  });

  test('not render anything when the id is not valid', () => {
    const Dummy = <div />;
    td.when(getCurrentOrganizationId()).thenReturn(null);
    const { wrapper } = setup({ children: Dummy });

    expect(wrapper.find('div').length).toBe(0);
  });

  test('render something when the id is valid', () => {
    const Dummy = <div />;
    td.when(getCurrentOrganizationId()).thenReturn(1);
    const { wrapper } = setup({ children: Dummy });

    expect(wrapper.find('div').length).toBe(1);
  });
});

想法?

@AnaRobynn非常同意第 1 点和第 2 点!

我的容器通常非常简单:

// imports hidden

const mapStateToProps = (state, { userId }) => ({
  isLoading: isLoading(state),
  hasError: hasError(state),
  placeholders: getPlaceholders(userId)(state), // a list of some sort
  shouldDoStuff: shouldDoStuff(state),
});

const mapDispatchToProps = {
  asyncActionPlaceholder,
};

export const mergeProps = (stateProps, dispatchProps, ownProps) => {
  return {
    ...stateProps,
    ...dispatchProps,
    ...ownProps,
    onMount: () => {
      if (stateProps.shouldDoStuff) {
        dispatchProps.asyncActionPlaceholder(ownProps.userId);
      }
    },
  };
};

export default compose(
  connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps
  ),
  callOnMount(props => props.onMount())
)(SampleComponent);

所以我会

  1. 监视所有选择器和动作创建者
  2. 使用正确的 userId 调用了测试getPlaceholders spy
  3. 断言SampleComponent使用正确的道具渲染
  4. SampleComponent调用onMount属性并验证它在shouldDoStuff===true时调度模拟操作
  • 我会在SampleComponent的单独测试中针对道具进行条件渲染之类的东西
  • 我也更喜欢使用redux-mock-store来模拟商店,如configureMockStore([thunk])()但认为不这样做也可以......

TL;DR我基本上只使用正确的参数调用了测试选择器,成功发送了模拟操作(如果它们依赖于任何逻辑)并且使用正确的道具呈现子组件

@philihp

  • 我非常反对测试外部库,我更喜欢模拟一切并孤立地测试事物。
  • 我确信端到端测试会发现由库升级引起的任何重大问题。
  • 我不喜欢一起测试容器+选择器+动作创建器的原因是,您必须为所有其他重用它们的容器重新测试相同的选择器和动作创建器。 在这一点上,您最好向单元测试本身添加更多测试用例。

@dougbacelar您能否提供一些示例测试代码来测试您展示的组件。 你用的是酵素? 还有什么? 浅的? 山?

@dougbacelar

我非常反对测试外部库,我更喜欢模拟一切并孤立地测试事物。

需要时模拟,但如果你可以使用真实的东西,为什么不呢? 当某些调用很慢或有网络请求等副作用时,模拟很有用。

我不喜欢一起测试容器+选择器+动作创建器的原因是,您必须为所有其他重用它们的容器重新测试相同的选择器和动作创建器。 在这一点上,您最好向单元测试本身添加更多测试用例。

的,独立测试您的选择器、动作创建者。 用大量预期的输入彻底测试它们。

的,还要测试你的容器。 如果这意味着它们对选择器和动作创建器提供重叠覆盖,那并不是一件坏事。

这就是我的意思...

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { saveColor } from '../actions/save';
import { selectColors } from '../reducers/colors.js';
import ColorButtons from '../components/ColorButtons';
const ColorButtons = ({ colors, onClick }) => (
  <div>
    {colors.map(color => {
      <button type="button" key={color} onClick={onClick(color)}>{color}</button>
    })}
  </div>
);
ColorButtons.propTypes = {
  colors: PropTypes.arrayOf(PropTypes.string),
  onClick: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
  colors: selectColors(state.colors),
});
const mapDispatchToProps = dispatch => ({
  onClickColor: color => () => {
    dispatch(saveColor({ color }));
  },
});
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ColorButtons);
import React from 'react';
import { shallow } from 'enzyme';
import configureStore from 'redux-mock-store';
import ColorButtons from '../ColorButtons';
import { saveColor } from '../../actions/save';
import { selectColors } from '../../reducers/colors.js';
const buildStore = configureStore();
describe('<ColorButtons />', () => {
  let store;
  let wrapper;
  const initialState = { colors: ['red', 'blue'] };
  beforeEach(() => {
    store = buildStore(initialState);
    wrapper = shallow();
  });
  it('passes colors from state', () => {
    expect(wrapper.props().colors).toEqual(selectColors(initialState.colors));
  });
  it('can click yellow', () => {
    const color = 'yellow';
    wrapper.props().onClick(color)();
    expect(store.getActions()).toContainEqual(saveColor({ color }));
  });
});

在这里,这不是测试selectColorssaveColor可以获得的所有不同参数; 它正在测试创建<ColorButtons />时,它会获得我们期望它获得的属性。 绝对在另一个测试中测试那些。

import { selectColors } from '../colors.js';
describe('selectColors', () => {
  it('returns the colors', state => {
    expect(selectColors(['red'])).toEqual(['red'])
  })
  it('returns an empty array if null', state => {
    expect(selectColors(null)).toEqual([])
  })
  it('returns a flattened array', state => {
    expect(selectColors(['red', ['blue', 'cyan']])).toEqual(['red','blue','cyan'])
  })
})

我非常反对测试外部库,我更喜欢模拟一切并孤立地测试事物。

需要时模拟,但如果你可以使用真实的东西,为什么不呢? 当某些调用很慢或有网络请求等副作用时,模拟很有用。

部分不同意。
我从不模拟外部库(不要模拟你不拥有的东西),但我确实写了一个包装器,例如 axios。 在我的协作测试中,我可以模拟所有功能并确保一切都正确连接。

在我的协作测试中,我总是模拟模块、函数等
您的协作者调用的这些函数可以很容易地进行单元测试。

这就是我的意思...

也不同意。
您在这里隐式测试您的减速器和选择器。 如果这就是你想做的事,但我更喜欢模拟这些函数并检查它们是否被正确调用。 其余的由减速器/选择器处理。

这个线程是在https://martinfowler.com/articles/mocksArentStubs.html经典和 Mockist 测试中提出的重新散列点

同意。 锁定。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

jbri7357 picture jbri7357  ·  3评论

timdorr picture timdorr  ·  3评论

benoneal picture benoneal  ·  3评论

ilearnio picture ilearnio  ·  3评论

amorphius picture amorphius  ·  3评论