storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.55k stars 9.3k forks source link

Feature request: Mock imports #10142

Closed lifeiscontent closed 4 months ago

lifeiscontent commented 4 years ago

Is your feature request related to a problem? Please describe.

One thing I've noticed is I find myself mocking a lot of imports, e.g. if I am using apollo, I use the MockedProvider in storybook, or if I'm using next.js I will mock the next/router.

in jest, I can just do jest.mock('next/router') and it'll load a file from __mocks__/next/router.js

it would be nice if storybook could do this as well because then I wouldn't have to rearrange code just to make it easier to test in storybook.

Describe the solution you'd like

an API to mock imports and provide stubs in a different file

Describe alternatives you've considered None, as this is a pretty clear solution to a problem

Are you able to assist bring the feature to reality? maybe, I don't have a ton of experience with webpack, but I imagine that is where a portion of this feature would stem from.

Additional context N/A

lifeiscontent commented 4 years ago

Just to shed some more light on why I think this feature should exist, I just recently created an example app in next.js and in order to test the full pages in the project, I ended up having to re-export all the pages so I could import them into storybook without having an apollo client trying to send API calls to production: https://github.com/lifeiscontent/realworld/tree/master/web/src/pages

shilman commented 4 years ago

Love this feature and think it could make Storybook a lot more powerful. Let's figure out how to make it happen!

It should be pretty easy to mock something globally and then provide a convenience function for accessing the currently rendered story ID, as an 80/20 solution.

stale[bot] commented 4 years ago

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

JuanCaicedo commented 4 years ago

Hi! I'm new to this issue 😄 Makes me wonder if it would be possible to do this by tapping into storybook's custom webpack config.

My thinking is the dependency could be mocked out leveraging something like webpack shims.

One tricky thing is that the webpack config is exposed in the storybook settings, but most likely the user would like to define mocks inside their stories. Those two mechanism would probably need a way to communicate 🤔

Just thinking out loud here!

nickdandakis commented 4 years ago

Just pulled this off with a custom Storybook Webpack config, and a Webpack alias.

Example .storybook/main.js:

const path = require('path');

module.exports = {
  stories: ['../stories/**/*.stories.js'],
  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
  webpackFinal: async (config) => {
    config.resolve.alias['next'] = path.resolve(__dirname, './mockNext');
    return config;
  }
};

Example .storybook/mockNext/link.js:

function MockLink({
  href,
  children,
  ...props
}) {
  return (
    <a href={href} {...props}>
      {children}
    </a>
  );
}

export default MockLink;

I'm mocking Next's Link component here, but you should be able to mock any import with this pattern. Unsure if Storybook needs an API for this, but might be useful to add this pattern to the docs if @shilman approves.

developer239 commented 3 years ago

@nickdandakis I don't think this is a correct solution because we should be able to mock/stub imports in runtime.

Example:

If I want to test Component A with Storybook I shouldn't have to connect it to redux just because Component B requires it. I likely have a separate story where Component B is tested so it would be better if I could stub Component B and not worry about redux when working with Component A.

This is very similar to how I would test components with jest. 🙂

eric-burel commented 3 years ago

Just linking @lifeiscontent excellent addon for Next.js router specifically: https://storybook.js.org/addons/storybook-addon-next-router I'll test that today but it sounds great, I've also opened a related discussion on supporting full pages: https://github.com/vercel/next.js/discussions/28290 Mocking Next.js imports is a first necessary step for this.

For mocking imports more broadly, the way to go is definitely tweaking the Webpack config a bit. I am not necessarily fond of having something as advanced as Jest system, which I find very complex to understand.

scottrippey commented 2 years ago

I created next-router-mock to handle this, and it works well with Storybook. You can enable it globally via a decorator, or per-component via a context provider: https://www.npmjs.com/package/next-router-mock

william-will-angi commented 2 years ago

Hi all, I've been trying to experiment a bit with this as well. Has anyone figured out a more generic solution via webpack? I'm hoping to avoid an alias for all the modules we plan on mocking (there are a lot).

eric-burel commented 2 years ago

Hi all, I've been trying to experiment a bit with this as well. Has anyone figured out a more generic solution via webpack? I'm hoping to avoid an alias for all the modules we plan on mocking (there are a lot).

Can you elaborate maybe? For Meteor what we did was pointing all meteor/* imports to the right files or scrapping them. In my experience, using Webpack was not the most suited approach (it's bad at rewriting imports) and Babel would have been more appropriate.

If you use aliases instead, you can definitely generate your webpack alias automatically for instance. What's the reason for having a lot of mocked packages that wouldn't work with Next?

william-will-angi commented 2 years ago

Thanks for the response! We have a mono-repo with a package structure like this:

packages/
  package-1/
    index.js
    __mocks__/
      index.js
  package-2/
    index.js
    __mocks__/
      index.js
  package-3/
    index.js
    __mocks__/
      index.js

In jest all of the imports to mocks get updated automagically. However with webpack I'm hoping for a single config that avoids a manual alias for every mock. i.e. I don't want to end up with the following:

config.resolve.alias['package-1'] = path.resolve(__dirname, './packages/package-1');
config.resolve.alias['package-2'] = path.resolve(__dirname, './packages/package-2');
config.resolve.alias['package-3'] = path.resolve(__dirname, './packages/package-3');
...

I think an automatic generation of aliases by parsing the file structure of the repo would be possible. My understanding is that the webpack config gets computed at build time so if I create __mocks__ directory while the server is running, I would think I need a reboot (maybe not the worst thing).

I took a look at your css-loaders and I think I could modify them to get the functionality I'm looking for, but may be overkill for my situation. I was hoping webpack may have another solution but it seems not.

eric-burel commented 2 years ago

Thanks for the response! We have a mono-repo with a package structure like this:

packages/
  package-1/
    index.js
    __mocks__/
      index.js
  package-2/
    index.js
    __mocks__/
      index.js
  package-3/
    index.js
    __mocks__/
      index.js

In jest all of the imports to mocks get updated automagically. However with webpack I'm hoping for a single config that avoids a manual alias for every mock. i.e. I don't want to end up with the following:

config.resolve.alias['package-1'] = path.resolve(__dirname, './packages/package-1');
config.resolve.alias['package-2'] = path.resolve(__dirname, './packages/package-2');
config.resolve.alias['package-3'] = path.resolve(__dirname, './packages/package-3');
...

I think an automatic generation of aliases by parsing the file structure of the repo would be possible. My understanding is that the webpack config gets computed at build time so if I create __mocks__ directory while the server is running, I would think I need a reboot (maybe not the worst thing).

I took a look at your css-loaders and I think I could modify them to get the functionality I'm looking for, but may be overkill for my situation. I was hoping webpack may have another solution but it seems not.

Let me know if you have something open source, I maintain a monorepo as well: https://github.com/VulcanJS/vulcan-npm

However I try to store global testing/storybook related config and mock stuff at top level to avoid this as much as possible (except of course the stories and unit test themselves), are often global mocks are roughly the same for all packages.

SoraKumo001 commented 1 year ago

Addon created to mock the module. https://github.com/SoraKumo001/storybook-module-mock

import Link from "next/link";
import React, { FC } from "react";

interface Props {}

/**
 * NextHook
 *
 * @param {Props} { }
 */
export const NextHook: FC<Props> = ({}) => {
  return (
    <div>
      <Link href="/">Before</Link>
    </div>
  );
};
import { expect } from "@storybook/jest";
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import { within } from "@storybook/testing-library";
import * as link from "next/link";
import { createMock, getMock } from "storybook-addon-module-mock";
import { NextHook } from "./NextHook";

const meta: ComponentMeta<typeof NextHook> = {
  title: "Components/NextHook",
  component: NextHook,
};
export default meta;

export const Primary: ComponentStoryObj<typeof NextHook> = {};

export const Mock: ComponentStoryObj<typeof NextHook> = {
  parameters: {
    moduleMock: {
      mock: () => {
        const mock = createMock(link);
        mock.mockReturnValue(<div>After</div>);
        return [mock];
      },
    },
  },
  play: async ({ canvasElement, parameters }) => {
    const canvas = within(canvasElement);
    expect(canvas.getByText("After")).toBeInTheDocument();
    const mock = getMock(parameters, link);
    expect(mock).toBeCalled();
  },
};

image image

kasir-barati commented 1 year ago

Just pulled this off with a custom Storybook Webpack config, and a Webpack alias.

Example .storybook/main.js:

const path = require('path');

module.exports = {
  stories: ['../stories/**/*.stories.js'],
  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
  webpackFinal: async (config) => {
    config.resolve.alias['next'] = path.resolve(__dirname, './mockNext');
    return config;
  }
};

Example .storybook/mockNext/link.js:

function MockLink({
  href,
  children,
  ...props
}) {
  return (
    <a href={href} {...props}>
      {children}
    </a>
  );
}

export default MockLink;

I'm mocking Next's Link component here, but you should be able to mock any import with this pattern. Unsure if Storybook needs an API for this, but might be useful to add this pattern to the docs if @shilman approves.

I have tiny issue with this solution, when you have TS aliases it ignores this mocks, or at least that is what happened in my case. I have storybook configured to be able to read tsconfig aliases and they are working but when I am trying to mock an alias import I have this issue that storybook cannot see them so it goes and uses the actual code and not the mock one 😞 Thanks @SoraKumo001 for introducing this addon but TBF I just need to achieve a simple task for this and it should be possible with this solution, but it is a bit problematic since I wanna mock an alias import.

Do you @SoraKumo001, @nickdandakis - have any idea how can I mock the aliases' imports in storybook?

SoraKumo001 commented 1 year ago

Mock may work if you apply Mock to 'mockNext/link.js'

kasir-barati commented 1 year ago

@SoraKumo001 I did not get your suggestion. Here is my setup, I have a tsconf like this:

// ...
"paths": {
  "@libs/*": ["libs/*"],
}
// ...

Does not work

and in my react component:

import {something} from "@libs/something"
import {useGetUser} from "@libs/hooks/use-get-user"

export function Component() { const user = useGetUser(); }

And here is my main.js

module.exports = {
  // ...
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "@libs/hooks/use-get-user": path.resolve(__dirname, "./mocked-use-get-user"),
  }
  // ...
}

and here is my stories.tsx

import {somethingElse} from "@libs/somthing-else";
import {Component} from "./component"

export default {
  component: Component,
  render: <Component />
}

Work

and in my react component:

import {something} from "@libs/something"
import {useGetUser} from "../../../libs/hooks/use-get-user"

export function Component() { const user = useGetUser(); }

And here is my main.js

module.exports = {
  // ...
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "../../../libs/hooks/use-get-user": path.resolve(__dirname, "./mocked-use-get-user"),
  }
  // ...
}

and here is my stories.tsx

import {somethingElse} from "@libs/somthing-else";
import {Component} from "./component"

export default {
  component: Component,
  render: <Component />
}

So now I wanna know why it is not working. JFI my TS aliases are working in general but just when I try to mock them they do not work

SoraKumo001 commented 1 year ago

Try the following

kasir-barati commented 1 year ago

@SoraKumo001 I did not get what you mean by storybook side, are you talking about .storybook/main.js or in the stories.tsx?

If the latter I should say that I do not know how I can mock it since it is not like the jest.mock, but also inside the component I am using it and not passing it as a prop or something similar.

If you meant the former I should say that in that case it just ignores the "... /... /... /libs/hooks/use-get-user" altogether since it does not match the import in the component.tsx

Here is what I understood when you said storybook side; open .storybook/main.js and in the webpackFinal config the mocked value. But

This does not work

component.tsx

import {something} from "@libs/something"
import {useGetUser} from "@libs/hooks/use-get-user"

export function Component() { const user = useGetUser(); }

stories.tsx

import {somethingElse} from "@libs/somthing-else";
import {Component} from "./component"

export default {
  component: Component,
  render: <Component />
}

.storybook/main.js

module.exports = {
  // ...
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "../../../libs/hooks/use-get-user": path.resolve(__dirname, "./mocked-use-get-user"),
  }
  // ...
}

Because the key ("../../../libs/hooks/use-get-user") does not match the actual import in the component.tsx ("@libs/hooks/use-get-user")

SoraKumo001 commented 1 year ago

I specified paths in tsconfig.json and it worked without any problems.

    "paths": {
      "@/*": ["./src/*"]
    }

https://github.com/SoraKumo001/storybook-module-mock/blob/master/src/components/Paths/Paths.stories.tsx https://sorakumo001.github.io/storybook-module-mock/?path=/story/components-paths--path-mock

gentlee commented 1 year ago

Just pulled this off with a custom Storybook Webpack config, and a Webpack alias.

Example .storybook/main.js:

const path = require('path');

module.exports = {
  stories: ['../stories/**/*.stories.js'],
  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
  webpackFinal: async (config) => {
    config.resolve.alias['next'] = path.resolve(__dirname, './mockNext');
    return config;
  }
};

Example .storybook/mockNext/link.js:

function MockLink({
  href,
  children,
  ...props
}) {
  return (
    <a href={href} {...props}>
      {children}
    </a>
  );
}

export default MockLink;

I'm mocking Next's Link component here, but you should be able to mock any import with this pattern. Unsure if Storybook needs an API for this, but might be useful to add this pattern to the docs if @shilman approves.

And what if I need to mock something in each Story differently?

tillsanders commented 9 months ago

I needed to mock the composables from vue-router and used the viteFinal configuration option. The component I was writing the story for imports these composables like this:

import { useRoute, useRouter } from 'vue-router'

This would be easy to mock in e.g. Vitest, but in Storybook this seems to require more work. There is a plugin that can mock imports, but only relative ones. So instead, I extended the vite config like this:

// .storybook/main.ts

import path from "path";
import type { StorybookConfig } from "@storybook/vue3-vite";
import { mergeConfig } from 'vite';

const config: StorybookConfig = {
  // [...]
  async viteFinal (config) {
    return mergeConfig(config, {
      resolve: {
        alias: {
          "vue-router": path.resolve(__dirname, "./mocks/vue-router"),
        },
      }
    });
  }
};
export default config;

And then I created a stub for the composables I needed:

// .storybook/mocks/vue-router.ts

export const useRouter = () => ({
  push: () => {}
});

export const useRoute = () => ({
  query: {}
});

This works for me. However, it would be nice to have a solution as simple as storybook.mock('vue-router').

valentinpalkovic commented 8 months ago

Please use the addon https://github.com/ReactLibraries/storybook-addon-module-mock for module mocking. Please open an issue there if it doesn't satisfy your use case. Closing. Please let me know if I should reopen the issue if you feel strongly about it and if the addon doesn't help.

rChaoz commented 6 months ago

@valentinpalkovic I don't think this should be closed. The add-on you provided is React only, and right now there's no way to achieve mocking in frameworks that don't use Webpack (e.g. Svelte)

vanessayuenn commented 5 months ago

@kasperpeulen can you confirm that this has been addressed as part of module mocking project?

vanessayuenn commented 4 months ago

Closing this as it has been addressed! Check out Type-safe module mocking in Storybook.