不变违规:在“Connect(SportsDatabase)”的上下文或props中找不到“商店”

IT技术 reactjs mocha.js redux
2021-03-23 22:46:36

完整代码在这里:https : //gist.github.com/js08/0ec3d70dfda76d7e9fb4

你好,

  • 我有一个应用程序,它根据构建环境显示桌面和移动设备的不同模板。
  • 我能够成功地在需要隐藏移动模板导航菜单的地方开发它。
  • 现在我可以编写一个测试用例,它通过 proptypes 获取所有值并正确呈现
  • 但不确定如何在其移动设备不呈现导航组件时编写单元测试用例。
  • 我试过了,但我遇到了一个错误...你能告诉我如何解决它。
  • 下面提供代码。

测试用例

import {expect} from 'chai';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import {SportsTopPortion} from '../../../src/components/sports-top-portion/sports-top-portion.jsx';
require('../../test-utils/dom');


describe('"sports-top-portion" Unit Tests', function() {
    let shallowRenderer = TestUtils.createRenderer();

    let sportsContentContainerLayout ='mobile';
    let sportsContentContainerProfile = {'exists': 'hasSidebar'};
    let sportsContentContainerAuthExchange = {hasValidAccessToken: true};
    let sportsContentContainerHasValidAccessToken ='test'; 

    it('should render correctly', () => {
        shallowRenderer.render(<SportsTopPortion sportsWholeFramework={sportsContentContainerLayout} sportsPlayers={sportsContentContainerProfile} sportsAuthentication={sportsContentContainerAuthExchange} sportsUpperBar={{activeSportsLink:'test'}} />);
        //shallowRenderer.render(<SportsTopPortion sportsWholeFramework={sportsContentContainerLayout} sportsPlayers={sportsContentContainerProfile} hasValidAccessToken={sportsContentContainerHasValidAccessToken}  />);

        let renderedElement = shallowRenderer.getRenderOutput();
        console.log("renderedElement------->" + JSON.stringify(renderedElement));

        expect(renderedElement).to.exist;
    });

    it('should not render sportsNavigationComponent when sports.build is mobile', () => {
        let sportsNavigationComponent = TestUtils.renderIntoDocument(<SportsTopPortion sportsWholeFramework={sportsContentContainerLayout} sportsPlayers={sportsContentContainerProfile} sportsAuthentication={sportsContentContainerAuthExchange} sportsUpperBar={{activeSportsLink:'test'}} />);
        console.log("sportsNavigationComponent------->" + JSON.stringify(sportsNavigationComponent));

        //let footnoteContainer = TestUtils.findRenderedDOMComponentWithClass(sportsNavigationComponent, 'linkPack--standard');

        //expect(footnoteContainer).to.exist;
    });

});

需要编写测试用例的代码片段

if (sports.build === 'mobile') {
    sportsNavigationComponent = <div />;
    sportsSideMEnu = <div />;
    searchComponent = <div />;
    sportsPlayersWidget = <div />;
}

错误

1) "sports-top-portion" Unit Tests should not render sportsNavigationComponent when sports.build is mobile:
     Invariant Violation: Could not find "store" in either the context or props of "Connect(SportsDatabase)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(SportsDatabase)".
      at Object.invariant [as default] (C:\sports-whole-page\node_modules\invariant\invariant.js:42:15)
      at new Connect (C:\sports-whole-page\node_modules\react-redux\lib\components\createConnect.js:135:33)
      at [object Object].ReactCompositeComponentMixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactCompositeComponent.js:148:18)
      at [object Object].wrapper [as mountComponent] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (C:\sports-whole-page\node_modules\react\lib\ReactMultiChild.js:241:44)
      at ReactDOMComponent.Mixin._createContentMarkup (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:591:32)
      at ReactDOMComponent.Mixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:479:29)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (C:\sports-whole-page\node_modules\react\lib\ReactMultiChild.js:241:44)
      at ReactDOMComponent.Mixin._createContentMarkup (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:591:32)
      at ReactDOMComponent.Mixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:479:29)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at [object Object].ReactCompositeComponentMixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactCompositeComponent.js:225:34)
      at [object Object].wrapper [as mountComponent] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (C:\sports-whole-page\node_modules\react\lib\ReactMultiChild.js:241:44)
      at ReactDOMComponent.Mixin._createContentMarkup (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:591:32)
      at ReactDOMComponent.Mixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:479:29)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at [object Object].ReactCompositeComponentMixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactCompositeComponent.js:225:34)
      at [object Object].wrapper [as mountComponent] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (C:\sports-whole-page\node_modules\react\lib\ReactMultiChild.js:241:44)
      at ReactDOMComponent.Mixin._createContentMarkup (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:591:32)
      at ReactDOMComponent.Mixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactDOMComponent.js:479:29)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at [object Object].ReactCompositeComponentMixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactCompositeComponent.js:225:34)
      at [object Object].wrapper [as mountComponent] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at [object Object].ReactCompositeComponentMixin.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactCompositeComponent.js:225:34)
      at [object Object].wrapper [as mountComponent] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactReconciler.mountComponent (C:\sports-whole-page\node_modules\react\lib\ReactReconciler.js:37:35)
      at mountComponentIntoNode (C:\sports-whole-page\node_modules\react\lib\ReactMount.js:266:32)
      at ReactReconcileTransaction.Mixin.perform (C:\sports-whole-page\node_modules\react\lib\Transaction.js:136:20)
      at batchedMountComponentIntoNode (C:\sports-whole-page\node_modules\react\lib\ReactMount.js:282:15)
      at ReactDefaultBatchingStrategyTransaction.Mixin.perform (C:\sports-whole-page\node_modules\react\lib\Transaction.js:136:20)
      at Object.ReactDefaultBatchingStrategy.batchedUpdates (C:\sports-whole-page\node_modules\react\lib\ReactDefaultBatchingStrategy.js:62:19)
      at Object.batchedUpdates (C:\sports-whole-page\node_modules\react\lib\ReactUpdates.js:94:20)
      at Object.ReactMount._renderNewRootComponent (C:\sports-whole-page\node_modules\react\lib\ReactMount.js:476:18)
      at Object.wrapper [as _renderNewRootComponent] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactMount._renderSubtreeIntoContainer (C:\sports-whole-page\node_modules\react\lib\ReactMount.js:550:32)
      at Object.ReactMount.render (C:\sports-whole-page\node_modules\react\lib\ReactMount.js:570:23)
      at Object.wrapper [as render] (C:\sports-whole-page\node_modules\react\lib\ReactPerf.js:66:21)
      at Object.ReactTestUtils.renderIntoDocument (C:\sports-whole-page\node_modules\react\lib\ReactTestUtils.js:76:21)
      at Context.<anonymous> (C:/codebase/sports-whole-page/test/components/sports-top-portion/sports-top-portion-unit-tests.js:28:41)
      at callFn (C:\sports-whole-page\node_modules\mocha\lib\runnable.js:286:21)
      at Test.Runnable.run (C:\sports-whole-page\node_modules\mocha\lib\runnable.js:279:7)
      at Runner.runTest (C:\sports-whole-page\node_modules\mocha\lib\runner.js:421:10)
      at C:\sports-whole-page\node_modules\mocha\lib\runner.js:528:12
      at next (C:\sports-whole-page\node_modules\mocha\lib\runner.js:341:14)
      at C:\sports-whole-page\node_modules\mocha\lib\runner.js:351:7
      at next (C:\sports-whole-page\node_modules\mocha\lib\runner.js:283:14)
      at Immediate._onImmediate (C:\sports-whole-page\node_modules\mocha\lib\runner.js:319:5)
6个回答

这很简单。您正在尝试测试通过调用生成的包装器组件connect()(MyPlainComponent)该包装器组件希望能够访问 Redux 存储。通常,该商店可用作context.store,因为在组件层次结构的顶部,您会有一个<Provider store={myStore} />. 但是,您自己渲染连接的组件,没有存储,因此会引发错误。

你有几个选择:

  • 创建一个 store 并<Provider>围绕连接的组件渲染一个
  • 创建一个 store 并直接传入 as <MyConnectedComponent store={store} />,因为连接的组件也会接受“store”作为 prop
  • 不要费心测试连接的组件。导出“普通”、未连接的版本,然后对其进行测试。如果您测试您的普通组件和您的mapStateToProps功能,您可以放心地假设连接的版本将正常工作。

您可能想通读 Redux 文档中的“测试”页面:https : //redux.js.org/recipes/writing-tests

编辑

在实际看到您发布的源代码并重新阅读错误消息后,真正的问题不在于 SportsTopPane 组件。问题是您正在尝试“完全”渲染 SportsTopPane,它也会渲染其所有子项,而不是像在第一种情况下那样进行“浅”渲染。该行searchComponent = <SportsDatabase sportsWholeFramework="desktop" />;正在渲染一个我认为也已连接的组件,因此期望在 React 的“上下文”功能中提供一个商店。

此时,您有两个新选项:

  • 只对 SportsTopPane 进行“浅”渲染,这样您就不会强迫它完全渲染其子项
  • 如果您确实想要对 SportsTopPane 进行“深度”渲染,则需要在上下文中提供 Redux 存储。我强烈建议您查看酶测试库,它可以让您做到这一点。有关示例,请参阅http://airbnb.io/enzyme/docs/api/ReactWrapper/setContext.html

总的来说,我会注意到您可能试图在这个组件中做太多事情,并且可能想要考虑将它分解成更小的部分,每个组件的逻辑更少。

啊哈。现在我明白发生了什么。问题不在于 SportsTopPane 本身。问题是您正在对 SportsTopPane 进行“完整”渲染,而不是“浅层”渲染,因此 React 正在尝试完全渲染所有子项。错误消息指的是行searchComponent = <SportsDatabase sportsWholeFramework="desktop" />;是期待存储和中断的连接组件。所以,有两个新建议:要么只做 SportsTopPane 的浅层渲染,要么使用 Enzyme 之类的库来测试。请参阅airbnb.io/enzyme/docs/api/ReactWrapper/setContext.html
2021-05-29 22:46:36
你能告诉我如何为这个场景编写测试用例吗``` 但不确定当它的移动设备不应该呈现导航组件时如何编写单元测试用例。``
2021-06-07 22:46:36
我假设在 SportsTopPortion.js 中,您有let SportsTopPortion = connect(mapStateToProps)(SomeOtherComponent). 最简单的答案是测试其他组件,而不是connect.
2021-06-08 22:46:36
回答对“这很简单”这句话感到困惑或困惑的人可能会显得轻蔑或苛刻。请谨慎使用。
2021-06-13 22:46:36
我试过了,但不知道该怎么做……你能在我的测试用例中更新吗
2021-06-14 22:46:36

开玩笑的对我有用的可能解决方案

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);
    });
});
效果很好,而不必一个一个地传递道具。
2021-06-17 22:46:36
谢谢,非常好的解决方案。我遇到了这个问题,因为我使用的是带有路由的顶级 App 组件,并且商店在每个路由中提供给子应用程序,因此我不必将道具传递到路由器中。我对它进行了一些更改以供我使用。const wrapper = Shallow(<Provider store={store}><App /></Provider>); 期望(wrapper.contains(<App />)).toBe(true);
2021-06-17 22:46:36

正如redux的官方文档所建议的那样,最好也导出未连接的组件。

为了能够在不处理装饰器的情况下测试 App 组件本身,我们建议您也导出未装饰的组件:

import { connect } from 'react-redux'

// Use named export for unconnected component (for tests)
export class App extends Component { /* ... */ }
 
// Use default export for the connected component (for app)
export default connect(mapStateToProps)(App)

由于默认导出仍然是装饰组件,上图中的导入语句将像以前一样工作,因此您不必更改应用程序代码。但是,您现在可以在测试文件中导入未修饰的 App 组件,如下所示:

// Note the curly braces: grab the named export instead of default export
import { App } from './App'

如果您同时需要:

import ConnectedApp, { App } from './App'

在应用程序本身中,您仍然可以正常导入它:

import App from './App'

您只会将命名导出用于测试。

这个答案也是合法的。编辑您的链接以匹配锚点。
2021-05-23 22:46:36
这是让我的测试再次通过的最快和最简单的方法。
2021-05-26 22:46:36
谢谢@lokori 很高兴你喜欢它!
2021-06-06 22:46:36
“您只能将命名导出用于测试。” -- 对我有用。
2021-06-12 22:46:36
这个答案很有道理!在所有情况下它可能都不是正确的事情,但绝对比在没有必要时与提供者一起玩以及所有这些更好。
2021-06-17 22:46:36

当我们将 react-redux 应用程序放在一起时,我们应该会看到一个结构,在顶部我们有一个Provider标签,其中包含一个 redux 存储的实例。

Provider然后标签呈现您的父组件,让我们将其称为App组件,组件依次呈现应用程序内的所有其他组件。

这是关键部分,当我们用connect()函数包装一个组件时,该connect()函数希望看到层次结构中具有Provider标记的某个父组件

所以你把connect()函数放在那里的实例,它会查找层次结构并尝试找到Provider.

这就是您想要发生的事情,但是在您的测试环境中,流程正在崩溃。

为什么?

为什么?

当我们回到假定的sportsDatabase 测试文件时,您必须自己是sportsDatabase 组件,然后尝试单独呈现该组件。

所以本质上,您在该测试文件中所做的只是获取该组件并将其随意扔掉Provider,它与上面的任何或存储都没有关系,这就是您看到此消息的原因。

Provider该组件的上下文或 prop 中没有 store 或tag,因此该组件会抛出错误,因为它想Provider在其父层次结构中查看tag 或 store。

所以这就是那个错误的意思。

就我而言

const myReducers = combineReducers({
  user: UserReducer
});

const store: any = createStore(
  myReducers,
  applyMiddleware(thunk)
);

shallow(<Login />, { context: { store } });