[RFC] - Mocking Angular dependencies provided at component level #31088
Replies: 2 comments 1 reply
-
@storybookjs/angular Would you like to take a look? :) |
Beta Was this translation helpful? Give feedback.
-
@christoph-rogalla Very nice write up! Have you thought about the story authoring API for this when using TestBed? There are two ways of writing Angular stories 1. Provide componentconst meta: Meta = {
/* ... */
component: DITestComponent
}
export default meta;
export const Foo: StoryObj = {
args: { /* ... */ }
}
export const Bar: StoryObj = {
args: { /* ... */ }
} I don't know if that's even possible but with TestBed, it could look kinda like this maybe? await TestBed.configureTestingModule({}).overrideComponent(DITestComponent, {
set: {
providers: [
{ provide: RealService, useClass: MockedService },
]
}
}).compileComponents();
fixture = TestBed.createComponent(DITestComponent);
const meta: Meta = {
/* ... */
component: fixture.componentRef
}
export default meta;
export const Foo: StoryObj = {
args: { /* ... */ }
}
export const Bar: StoryObj = {
args: { /* ... */ }
} 2. Provide templateconst meta: Meta = {
/* ... */
}
export default meta;
export const Foo: StoryObj = {
render: () => ({ template: `<app-di-test />` }),
args: { /* ... */ }
}
export const Bar: StoryObj = {
render: () => ({ template: `<app-di-test />` }),
args: { /* ... */ }
} For templates, it probably means TestBed needs to be an internal thing within storybook/angular. The downside: It does not scale with the latest Angular APIs and Storybook would need to catch up every time the next new API gets published But ending the thought process, maybe something like this could also work for both, component and template, cases? const meta: Meta = {
component: DITestComponent,
/* Internally, Storybook creates the DITestComponent using TestBed and applies the overwriteProviders to it */
overwriteProviders: []
}
export default meta;
export const Foo: StoryObj = {
render: () => ({ template: `<app-di-test />` }),
/* Here, it would apply the overwrite to the wrapper component that also is created by TestBed internally */
overwriteProviders: []
args: { /* ... */ }
} Or is this even what you suggested @christoph-rogalla ? |
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
Many components have dependencies that are required for communication with the server or for state management. If these are provided within the component, it is currently not possible to mock them. This is due to the interaction between Storybook's implementation and Angular's dependency hierarchy. The aim is to make this possible again using the testbed library, which provides all the functionality to solve this problem and could lead to the abstraction of the current wrapper section inside Storybook’s implementation.
Problem Statement
Storybook uses an internal wrapper module and component to ensure that change detection and metadata are configured correctly. These use observables to make sure that attributes of the component are modifiable via the Storybook UI and that various dependencies are loadable via the module. The result is a certain hierarchy within the construct.
The moduleMetaData function allows you to define dependencies and inject mock services. However, because of Angular's inject hierarchy and the fact that dependencies are provided inside the wrapper section, the story component cannot resolve the services that are configured individually. (#22352) Angular first looks inside the component itself and then tries to evaluate the dependencies further up the component tree until it reaches the environment provider or null injector. This results in providing the original service instead of the mocked one, because the original one is closer than the externally configured one.
Example:
A story with this component is only limited possible under certain circumstances. It is not possible to mock the used service via the Storybook user interface. It is especially difficult if this service has other dependencies that are not provided within the component. In this case, using Storybook will result in a provider error and the component can only be integrated in a roundabout way. This issue is particularly critical with regard to the standardization of standalone components.
Non-goals
This fix should not change the core of the Storybook implementation and should focus on the wrapper section. Additionally, it should be an Angular-specific fix and won't change the other framework related dependency mocking functionalities. In addition, the primary objective is the availability of the services being simulated, rather than performance. Also, the structure of the modulemetaData function should not be changed in order to avoid users having to make adjustments.
Implementation
The idea is to use the TestBed-API to solve the problem. It provides the functionality that Storybook lacks. TestBed lets you override a component's dependencies and set up a testing module around the component. It also renders components' HTML based on parameters inside its TypeScript class and provides a way for change detection.
The fixture TestBed is returning provides the component instance as well as the native-Element. Storybook already has a wrapper module and component which could potentially be replaced by the Testbed component or, at the very least, be integrated into it. The bootstrap process is also performed within the rendering process. Angular's bootstrapApplication function is used, which only accepts standalone components. The Testbeds Inject method can be used to get an ApplicationRef object, which can be used to process any type of component. Also, such an object is needed for the rest of the process, so it can be reused. Although TestBed is generally used as a singleton, Angular offers the possibility to create your own instance. This could be saved within the abstract renderer and reused when calling it again or updating attributes.
Prior Art
Angular component tests use the testbed API, making it easier for Angular users to find their way around the storybook implementation. Frameworks such as Spectator enable the mocking of dependencies too. However, this requires a definition of the methods used and their return types, which in turn could lead to increased effort on the part of storybook users. Most comparable frameworks or libraries use TestBed in connection with dependencies in Angular. Approaches that do not do so require external definition of methods or other behavior. By using TestBed, the dependency management would not be changed externally.
Deliverables
The AbstractRenderer needs to be updated to match these changes. The applicationRef and dependencies can be managed using the TestBed API. If a story does not require a complete render process, the responsible testbed instance should be updated. A wrapper component is still required for stories of pure HTML or similar non-Angular components. However, this can be greatly simplified, as a large part of the configuration can be carried out via the TestBed API.
Risks
The Testbed-API provides most of the feature storybook needs; however, it remains unclear how much of the implementation can be removed and replaced by TestBed without altering the entire Renderer. The wrapper module of the TestBed api allows the configuration of various metadata, but it does not provide the possibility to define exports of the module. This could cause problems with pipes or similar Angular constructs that are not standalone.
Unresolved Questions
Alternatives considered / Abandoned Ideas
An alternative approach involves the utilization of the Testing Library. In essence, it is not significantly different from the use of Testbed, as it is an abstraction of the Testbed library. However, it offers a range of methods that provide greater value, particularly in terms of storybook.
The test library enables direct overwriting of component-level metadata and provides options to partially or completely update component attributes while triggering the Angular lifecycle hooks. Nonetheless, the primary benefit of the TestBed approach is the ability to customize implementation and maintenance by Google.
Beta Was this translation helpful? Give feedback.
All reactions