Replies: 8 comments 15 replies
-
I don't think there are actual use cases for inline mocking, it's more about familiarity. And AFAIK some Vitest maintainers are of the same opinion, they only have the functionality to be familiar with Jest, and not because they want to actually recommend setting different mocks per test/file. I like that
For those reasons, personally I'd lean towards ditching the One question I have though - which I think is relevant with our without the I guess in these (probably very rare) situations, our current subpath imports would help solve the issue? Allowing users to point Storybook towards another mock file, potentially? |
Beta Was this translation helpful? Give feedback.
-
As far as the colocation argument goes, doesn't the pattern of:
Allow for colocating the specifics of the mock with where it is used in the stories? The extra mock file is just boilerplate to enable the functionality. The important logic is colocated. And, of course, this pattern could also apply to the API proposed here. |
Beta Was this translation helpful? Give feedback.
-
I like the proposal. I agree with @JReinhold that there is no existing familiarity with the We could consider adding a import { mock } from 'storybook/test';
export default definePreview({
mocks: [
mock(import('../lib/db.ts')),
mock(import('package1')),
mock(import('../src/path/to/some/File.ts'), { spy: true })
],
}); This makes the API more familiar at first glance, but we can still keep it constrained:
Vitest also supports a module promise instead of a string in the vi.mock(import('./path/to/module.js')) |
Beta Was this translation helpful? Give feedback.
-
My initial impression of seeing the subpath module mocking (using the special Storybook imports directive) was that it's quite brittle and will lead to developers constantly modifying their package.jsons when working in Storybook. Instead, I encourage you to collocate module mocks with the layer that needs them, which in this case it's a story. // stories/Dashboard.stories.tsx
mock(moduleIdentifier, implementation) As to how to implement this, I think it's worth considering going ESM (if not already) and utilizing HTTP-based module mocking. Vitest does this to a great effect in the Browser Mode, where You can also alias |
Beta Was this translation helpful? Give feedback.
-
How is this supposed to be implemented? AST parsing on the preview file? |
Beta Was this translation helpful? Give feedback.
-
Great RFC. What I wonder (and we should make clear is):
If this helps in any way, for reference, here are two community addons that provide ways of mocking: |
Beta Was this translation helpful? Give feedback.
-
IIUC, this will only be compatible with the vitest-based test runner, and not with |
Beta Was this translation helpful? Give feedback.
-
how high is the combinatorial complexity of this RFC when considering:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
We introduced Typesafe module mocking in Storybook 8.1, which uses the Node.js subpath imports standard. We took this approach because it’s supported across the JS ecosystem, meaning that module mocked stories will work in both Vite/Webpack/Node/TS. Since then, we’ve learned that subpath imports can be challenging to set up, and require more manual work than users want. We also explored
vi.mock
inline module mocking, but it adds a lot of complexity and surface area. In this RFC, we propose a stylized module mocking approach that tries to strike a balance between the two solutions.Problem Statement
The biggest problems with subpath imports for module mocking are:
package.json
subpath imports.#
(e.g.#config/main.ts
), whereas many projects choose other characters like~
or@
.Non-goals
We also acknowledge that users want inline module mocking because they are most familiar with it. However, that solution has its own challenges and complexities, which is why we’re considering a simpler approach.
We want a simpler approach so that we can provide a module mocking approach that extends to other builders like Webpack/Rspack. However, enabling that today in Webpack/Rspack is also a non-goal of this RFC. We just want to preserve the future option. In the short term, Webpack/Rspack users should continue to use the subpath imports standard.
Implementation
While "happy path" inline mocking is straightforward, the API docs reveal a dizzying array of warnings and complexities.
The implementation of
vi.mock
is even more complex. And that's our primary concern with inline mocking. Though Storybook Test is Vitest-based and we are getting Vitest's implementation "for free", we want the option to extend module mocking to our Webpack/Rspack users in the future and we worry that will be prohibitively difficult withvi.mock
(or a hypotheticalsb.mock
).The key observation behind this proposal is that inline module mocking consists of two aspects:
vi.mock('my-module')
vi.mock('my-module', myFactoryFn)
,vi.mocked(my_module.x).mockImplementation(myFn)
We can simplify module mocking by decoupling these two concepts.
Declarative, global module mocking
We propose that mocking should be declarative and global rather than imperative and local.
Rather than writing
vi.mock
in each story file, you do it once in the project's preview file:This is equivalent to the following Vitest code:
It's a lot simpler because it happens once in your Storybook. The downside is that if you want to manually mock the module in one set of stories and leave that module unmocked in other stories, e.g. for performance reasons, that is not possible. The crux of this proposal is that this is an acceptable constraint.
Specifying the mock
While mocking is global and declarative, we believe that specifying the mock should be co-located with your stories/tests. That's what's great about inline mocking, and that's also possible in this setup:
Benefits
We believe that the above scheme is simple and solves all of the problems listed above:
More work.We’re no longer using subpath imports, so the user does not need to modifypackage.json
. Nor is there an extra mock file.Invasive.We no longer need absolute imports, so the user does not need to modify their application/components.Finicky.We’re no longer using subpath imports, so we shouldn’t have any issues with e.g. TypeScript aliases. If we are able to reuse Vitest’s mocking logic, we should be as robust as Vitest in this regard.File-based mocking (advanced)
This scheme is more restrictive than inline mocking, so by definition it cannot satisfy every single use case. But the question is whether it can solve every single important use case. We know of one such use case, and to solve it, we also propose file-based mocking as an advanced maneuver.
In Jest, when the user creates
path/to/__mocks__/File.ts
, it will automatically use it to mockpath/to/File.ts
. In Vitest, the same is true, but the user has to explicitly enable it withvi.mock
(see end of vi.mock docs).We propose using this existing behavior when the user wants to implement a specific module behavior in their tests. This has come up in our own work on component testing RSCs. In the case where an RSC accesses a Node-based database client, we want to swap out the database module for an in-memory, browser-based mock.
By creating a mock file,
lib/__mocks__/db.ts
that mocks out the Node-equivalentlib/db.ts
, we can ensure that the Node code is never loaded. The user would also need to declare this:This also matches the Vitest behavior, so although the syntax of
vi.mock
vsmocks.manual
is slightly different, conceptually and implementation-wise these should line up exactly.Prior Art
We are building on the shoulders of giants by repacking Vitest's module mocking in a slightly more restrictive way, which in turn builds on Jest manual mocks.
Deliverables
Risks
It’s possible that even after doing more work for the subpath imports solution, users still want inline module mocking and Storybook Test remains uncompetitive when it comes to module mocking.
It's also possible that there are specific use cases that are important but not covered by the above scheme.
It's also possible that even with the restrictive version of module mocking, we are still unable to implement this in Webpack/Rspack if the time comes that we want to.
Unresolved Questions
lib/__mocks__/db.ts
, or should we use the Storybook pattern oflib/db.mock.ts
which is non-standard but feels a little nicer?Alternatives considered / Abandoned Ideas
As mentioned above, we are also considering just supporting
vi.mock
directly, or potentiallysb.mock
which behaves identically. The main reason we're not doing this is that we don't want to commit to all ofvi.mock
's surface area.Beta Was this translation helpful? Give feedback.
All reactions