Storybook: Stub component?

Created on 16 Jun 2017  路  9Comments  路  Source: storybookjs/storybook

I have a question.
I'm wondering if I can stub sub-components when showing a component in storybook.

For example, assume a component defined like:

import SubComponent from './sub-component.jsx'

class Component extends React.Component {
  render() {
    return (
      <div>
        ...
        <SubComponent />
        ...
      </div>
    )
  }
}

The case is that the SubComponent is very complex and is difficult to browse in the same story,
or just show a story of the Component without showing SubComponent.
I want skip rendering the SubComponent.

Is there any idea?

question / support

Most helpful comment

I wrote an article how to stub components in Storybook https://medium.com/p/how-to-stub-vuejs-react-container-components-in-storybook-eb4193c48f46

TLDR:
The approach is similar to @rcadel's one, but uses a NormalModuleReplaccementPlugin plugin instead of a loader:

plugins: [
  new webpack.NormalModuleReplacementPlugin(
    /\.vue$/,
    resource => {
      const resPath = resource.request;
      const mockPath = resPath.slice(0, -3) + 'mock.vue';
      const absMockPath = path.resolve(resource.context, mockPath);
      const absRootMockPath = path.resolve(
        __dirname,
        './component-stub.vue'
      );
      if (fs.existsSync(absMockPath)) {
        resource.request = mockPath;
      } else if (resPath.endsWith('container.vue')) {
        resource.request = absRootMockPath;
      }
    }),
],

It checks all the .vue components and

  • uses the sibling .mock.vue fake compoent if there is one
  • uses the "root" .storybook/component-stub.vue fake component if the filename ends with container.vue.

The same logic can be applied to React components

All 9 comments

@MasahiroMorita Have you considered using mock functions, e.g. https://github.com/testdouble/testdouble.js/blob/master/docs/7-replacing-dependencies.md#testdoublereplace-api

I haven't tried this and don't think this code is exactly right, but it would look something like:

stories.add('mocked', () => {
  td.replace('path/to/SubComponent').when().thenReturn(<Stub/>);
  const Component = require('../path/to/Component');
  return <Component foo="bar" />;
});

The important part is that I'm using require to dynamically load Component, and I stub out SubCmoponent before I load component.

Let me know if that works. Could make an interesting add-on.

You can also do this with proxyquire and jest/sinon:

proxyquire('path/to/SubComponent', jest.fn(() => </Stub>));

There's probably a way to do it in pure jest also.

Great! I'll try proxyquire.
https://github.com/tomitrescak/proxyrequire

Thank you.

@MasahiroMorita great. please report back if you get it working! i think this could be a very interesting pattern for Storybook.

I'm going to close this for now, but please do let us know how it goes, and feel free to reopen if you
need more help. 馃憤

@shilman
I found this: https://github.com/Wildhoney/Mocktail
It is very easy to mock components.

You can define a Component like:

import React from 'react'
import { mock } from 'mocktail'
function SomeComponent(props) {
  return (<div>This is a component</div>)
}

export default mock(SomeComponent)

then in stories:

import { env, ENV, inject } from 'mocktail'
env(ENV.TESTING)
inject('SomeComponent', (props)=> <div>This is mock component</div>)
import SomeComponent from 'some-component'

storiesOf('SomeComponent', module)
  .add('some-component', ()=> <SomeComponent />)

this will substitute SomeComponent to the mock, and it will display 'This is mock component', rather than the original one.

It is very simple solution. I hope you will add some descriptions or usage how to mock components using mocktail in storybook manual.

I tried proxyquire, but it doesn't work with webpack.

I know this is an old issue but i spend some time trying to find a solution to this problems and i found one in this comment.

So my .storybook/webpack.config.js look like this

...
test: /\.(ts|tsx|js|jsx)$/,
    use: [
      {
        loader: require.resolve("babel-loader"),
        options: {
          presets: [["react-app", { flow: false, typescript: true }]]
        }
      },
      {
        loader: path.resolve("loaders/mockLoader.js")
      }
    ]
...

and my mockLoader.js look like this

var path = require("path");
var fs = require("fs");
var loaderUtils = require("loader-utils");
module.exports = function(source) {
  var result = source;
  if (this.resourcePath.indexOf("node_modules") === -1) {
    var mockFileName = loaderUtils.interpolateName(
      this,
      "[path][name].mock.[ext]",
      {}
    );
    if (fs.existsSync(mockFileName)) {
      //add this file to watch mode
      this.addDependency(mockFileName);
      result = fs.readFileSync(mockFileName);
    }
  }
  return result;
};

this way i just have to put a Component.mock.[ext] just right next to my Component.[ext] and in storybook webpack will load this implementation instead of original one

  • proxyquire doesn't work with webpack
  • Mocktail isn't that powerfull

There is another option, robust, well mantained and works very well

https://github.com/theKashey/rewiremock

I wrote an article how to stub components in Storybook https://medium.com/p/how-to-stub-vuejs-react-container-components-in-storybook-eb4193c48f46

TLDR:
The approach is similar to @rcadel's one, but uses a NormalModuleReplaccementPlugin plugin instead of a loader:

plugins: [
  new webpack.NormalModuleReplacementPlugin(
    /\.vue$/,
    resource => {
      const resPath = resource.request;
      const mockPath = resPath.slice(0, -3) + 'mock.vue';
      const absMockPath = path.resolve(resource.context, mockPath);
      const absRootMockPath = path.resolve(
        __dirname,
        './component-stub.vue'
      );
      if (fs.existsSync(absMockPath)) {
        resource.request = mockPath;
      } else if (resPath.endsWith('container.vue')) {
        resource.request = absRootMockPath;
      }
    }),
],

It checks all the .vue components and

  • uses the sibling .mock.vue fake compoent if there is one
  • uses the "root" .storybook/component-stub.vue fake component if the filename ends with container.vue.

The same logic can be applied to React components

I just used the approach outlined by @aantipov to mock an NPM dependency:

    {
        plugins: [
            new webpack.NormalModuleReplacementPlugin(
                /webextension-polyfill-ts/,
                resource => {
                    // Gets absolute path to mock `webextension-polyfill-ts` package
                    // NOTE: this is required beacuse the `webextension-polyfill-ts`
                    // package can't be used outside the environment provided by web extensions
                    const absRootMockPath = path.resolve(
                        __dirname,
                        "../src/__mocks__/webextension-polyfill-ts.ts",
                    );

                    // Gets relative path from requesting module to our mocked module
                    const relativePath = path.relative(
                        resource.context,
                        absRootMockPath,
                    );

                    // Updates the `resource.request` to reference our mocked module instead of the real one
                    resource.request = relativePath;
                },
            ),
        ],
    },

I just hit a snag where the webextension-polyfill-ts package was crashing Storybook because it can't be used outside of Web Extension environments - this fix allowed me to mock that module with my own copy. Works great :+1:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

firaskrichi picture firaskrichi  路  61Comments

Gongreg picture Gongreg  路  58Comments

maraisr picture maraisr  路  119Comments

bpeab picture bpeab  路  70Comments

moimikey picture moimikey  路  67Comments